Skip to content

Latest commit

 

History

History
211 lines (150 loc) · 9.87 KB

Overview.md

File metadata and controls

211 lines (150 loc) · 9.87 KB

Overview

Motivation

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.

Summary

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's Function, 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.

Type Representation

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 the kind field.

Naming of size limits

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 to minimum

  • 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.

Extensions to API functions

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 additional type 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)

Addition of WebAssembly.Function

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 of Function 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.

Example

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);