Wasm is typed, and its types carry information that can be useful and important to clients interacting with Wasm modules and objects through the JS API. For example, types describe the form of imports and exports, including the size limits of memories and tables or the mutability of globals. The desire to query information like this from JS has come up several times, for example, with the following issues:
For example, it is needed to write a JS-hosted linker or an adaptor mechanism for modules.
This proposal adds respective functionality to the JS API in a systematic manner.
In a nutshell, this proposal consists of three parts:
-
Define a representation of Wasm types as JS objects
-
Extend API classes with a
type
method to retrieve the type of the underlying Wasm object -
To that end, introduce
WebAssembly.Function
as a new class, subclassing JavaScript'sFunction
, to represent Wasm exported functions
The latter also provides a constructor for explicitly creating Wasm exported functions from regular JS functions. That enables JS code to put JS functions into a table, which is not currently possible.
All Wasm types can be defined by a simple grammar. This grammar could be mapped to JSON-style JS objects in a direct and extensible manner. For example, using TypeScript-style type definitions:
type RefType = "funcref" | "externref"
type ValueType = "i32" | "i64" | "f32" | "f64" | "v128" | RefType
type GlobalType = {value: ValueType, mutable: boolean}
type MemoryType = {limits: Limits}
type TableType = {limits: Limits, element: RefType}
type Limits = {min: number, max?: number} // see below
type FunctionType = {parameters: ValueType[], results: ValueType[]}
type ExternType =
{kind: "function", type: FunctionType} |
{kind: "memory", type: MemoryType} |
{kind: "table", type: TableType} |
{kind: "global", type: GlobalType}
Given the pre-existing JS API, we can repurpose (and rename) the existing descriptor interfaces of the API as types, and add the missing one for functions and extern types. The only difference to the above is that limits are inlined into memory and table types (and have longer names).
More concretely:
-
Rename ImportExportKind to ExternKind
-
Rename MemoryDescriptor to MemoryType
-
Rename TableDescriptor to TableType
-
Rename TableKind to ElemType
Note: These renamings of spec-internal definitions are purely cosmetic and do not affect the observable API.
-
Add a dictionary for function types:
dictionary FunctionType { required sequence<ValueType> parameters; required sequence<ValueType> results; };
-
Add a dictionary for external types:
dictionary ExternType { required ExternKind kind; required (FunctionType or TableType or MemoryType or GlobalType) type; };
As an additional constraint, the content of the
type
field must match that content of thekind
field.
There is one further quibble. The current definition of MemoryDescriptor and TableDescriptor names the attribute representing the minimum size initial
. That makes sense for its use as an argument to the respective constructor, but nowhere else: with the more general use as a type, this attribute merely reflects a current or minimum required size, possibly after growing. For imports in particular, the minimum size in their type may be larger than the initial size of an object matching that import (and smaller than its current size).
Hence, the descriptor currently used for table and memory constructors does not properly represent the notion of type. On the other hand, it is useful for constructors to directly understand the types delivered by the reflection functions (see the example below).
I hence propose to allow both minimum
and initial
as a name of that field. That is, they are both optional fields of the interface, but with the meta requirement that exactly one of them must be present. However, such a constraint cannot be epressed in WebIDL directly, but instead requires using auxiliary interfaces as follows:
-
In both MemoryDescriptor/Type and TableDescriptor/Type, rename
initial
tominimum
-
Change the parameter type of the Memory constructor to
(MemoryType or InitialMemoryType)
where InitialMemoryType corresponds to the current MemoryDescriptor -
Change the parameter type of the Table constructor to
(TableType or InitialTableType)
where InitialTableType corresponds to the current TableDescriptor
Note: The last two points are simply a backwards compatibility measure that enables the constructors to continue understanding initial
instead of minimum
as a field name.
Types can be queried by adding the following methods to the API.
-
Make ModuleExportDescriptor and ModuleImportDescriptor derive from
ExternType
:dictionary ModuleExportDescriptor : ExternType { ... }; dictionary ModuleImportDescriptor : ExternType { ... };
The
kind
field is removed from both definitions and instead inherited, along with the additionaltype
field. -
Extend interface Memory with attribute
MemoryType type();
-
Extend interface Table with attribute
TableType type();
-
Extend interface Global with
GlobalType type();
-
Overload constructor Memory (see above)
Constructor(MemoryType or InitialMemoryType type)
-
Overload constructor Table (see above)
Constructor(TableType or InitialTableType type)
-
Adjust constructor Global to accept a GlobalType and its initialisation value separately:
Constructor(GlobalType type, any value)
Currently, Wasm exported functions are not assigned a special class. Instead, they are simply have JavaScript's built-in class Function
.
This part of the proposal refines Wasm exported functions to have a suitable subclass, with the following advantages:
-
A
type
attribute can be added to this class, reflecting a Wasm function's type in a manner consistent with the other type reflection attributes proposed above. -
The constructor for this class can be used to explicitly construct Wasm exported functions, closing a gap in the current API in that does not provide a way for JavaScript to put a plain JS function into a table (while the same is possible from inside Wasm).
-
Wasm exported functions can be identified programmatically with an
instanceof
check.
Concretely, the change is the following:
-
Introduce a new class
WebAssembly.Function
that is a subclass ofFunction
as follows[LegacyNamespace=WebAssembly, Constructor(FunctionType type, function func), Exposed=(Window,Worker,Worklet)] interface Function : global.Function { FunctionType type(); };
-
All exported functions are of class
WebAssembly.Function
. -
Functions constructed by
WebAssembly.Function
behave no different from other exported functions taken from a module's exports. More specifically, they have a [[FunctionAddress]] internal slot which identifies them as exported functions.
The following function takes a WebAssembly.Module
and creates a suitable mock import object for instantiating it:
function mockImports(module) {
let mock = {};
for (let imp of WebAssembly.Module.imports(module)) {
let value;
switch (imp.kind) {
case "table":
value = new WebAssembly.Table(imp.type);
break;
case "memory":
value = new WebAssembly.Memory(imp.type);
break;
case "global":
value = new WebAssembly.Global(imp.type, undefined);
break;
case "function":
value = () => { throw "unimplemented" };
break;
}
if (! (imp.module in mock)) mock[imp.module] = {};
mock[imp.module][imp.name] = value;
}
return mock;
}
let module = ...;
let instance = WebAssembly.instantiate(module, mockImports(module));
The following example shows how to use the WebAssembly.Function
constructor to add a JavaScript function to a table, using multiple different types:
function print(...args) {
for (let x of args) console.log(x + "\n")
}
let table = new Table({element: "funcref", minimum: 10});
let print_i32 = new WebAssembly.Function({parameters: ["i32"], results: []}, print);
table.set(0, print_i32);
let print_f64 = new WebAssembly.Function({parameters: ["f64"], results: []}, print);
table.set(1, print_f64);
let print_i32_i32 = new WebAssembly.Function({parameters: ["i32", "i32"], results: []}, print);
table.set(2, print_i32_i32);