diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index b0cee058..55a3c2e2 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -3082,227 +3082,7 @@ In particular, the Component Model maintains the following invariants: ## JavaScript Embedding -### JS API - -The [JS API] currently provides `WebAssembly.compile(Streaming)` which take -raw bytes from an `ArrayBuffer` or `Response` object and produces -`WebAssembly.Module` objects that represent decoded and validated modules. To -natively support the Component Model, the JS API would be extended to allow -these same JS API functions to accept component binaries and produce new -`WebAssembly.Component` objects that represent decoded and validated -components. The [binary format of components](Binary.md) is designed to allow -modules and components to be distinguished by the first 8 bytes of the binary -(splitting the 32-bit [`core:version`] field into a 16-bit `version` field and -a 16-bit `layer` field with `0` for modules and `1` for components). - -Once compiled, a `WebAssembly.Component` could be instantiated using the -existing JS API `WebAssembly.instantiate(Streaming)`. Since components have the -same basic import/export structure as modules, this means extending the [*read -the imports*] logic to support single-level imports as well as imports of -modules, components and instances. Since the results of instantiating a -component is a record of JavaScript values, just like an instantiated module, -`WebAssembly.instantiate` would always produce a `WebAssembly.Instance` object -for both module and component arguments. - -Types are a new sort of definition that are not ([yet][type-imports]) present -in Core WebAssembly and so the [*read the imports*] and [*create an exports -object*] steps need to be expanded to cover them: - -For type exports, each type definition would export a JS constructor function. -This function would be callable iff a `[constructor]`-annotated function was -also exported. All `[method]`- and `[static]`-annotated functions would be -dynamically installed on the constructor's prototype chain, making sure to -register `[get]` and `[set]` functions as getters and setters. In the case of -re-exports and multiple exports of the same definition, the same constructor -function object would be exported (following the same rules as WebAssembly -Exported Functions today). In pathological cases (which, importantly, don't -concern the global namespace, but involve the same actual type definition being -imported and re-exported by multiple components), there can be collisions when -installing constructors, methods and statics on the same constructor function -object. In such cases, a conservative option is to undo the initial -installation and require all clients to instead use the full explicit names -as normal instance exports. - -For type imports, the constructors created by type exports would naturally -be importable. Additionally, certain JS- and Web-defined objects that correspond -to types (e.g., the `RegExp` and `ArrayBuffer` constructors or any Web IDL -[interface object]) could be imported. The `ToWebAssemblyValue` checks on -handle values mentioned below can then be defined to perform the associated -[internal slot] type test, thereby providing static type guarantees for -outgoing handles that can avoid runtime dynamic type tests. - -Lastly, when given a component binary, the compile-then-instantiate overloads -of `WebAssembly.instantiate(Streaming)` would inherit the compound behavior of -the abovementioned functions (again, using the `layer` field to eagerly -distinguish between modules and components). - -For example, the following component: -```wat -;; a.wasm -(component - (import "one" (func)) - (import "two" (value string)) ๐Ÿช™ - (import "three" (instance - (export "four" (instance - (export "five" (core module - (import "six" "a" (func)) - (import "six" "b" (func)) - )) - )) - )) - ... -) -``` -and module: -```wat -;; b.wasm -(module - (import "six" "a" (func)) - (import "six" "b" (func)) - ... -) -``` -could be successfully instantiated via: -```js -WebAssembly.instantiateStreaming(fetch('./a.wasm'), { - one: () => (), - two: "hi", ๐Ÿช™ - three: { - four: { - five: await WebAssembly.compileStreaming(fetch('./b.wasm')) - } - } -}); -``` - -The other significant addition to the JS API would be the expansion of the set -of WebAssembly types coerced to and from JavaScript values (by [`ToJSValue`] -and [`ToWebAssemblyValue`]) to include all of [`valtype`](#type-definitions). -At a high level, the additional coercions would be: - -| Type | `ToJSValue` | `ToWebAssemblyValue` | -| ---- | ----------- | -------------------- | -| `bool` | `true` or `false` | `ToBoolean` | -| `s8`, `s16`, `s32` | as a Number value | `ToInt8`, `ToInt16`, `ToInt32` | -| `u8`, `u16`, `u32` | as a Number value | `ToUint8`, `ToUint16`, `ToUint32` | -| `s64` | as a BigInt value | `ToBigInt64` | -| `u64` | as a BigInt value | `ToBigUint64` | -| `f32`, `f64` | as a Number value | `ToNumber` | -| `char` | same as [`USVString`] | same as [`USVString`], throw if the USV length is not 1 | -| `record` | TBD: maybe a [JS Record]? | same as [`dictionary`] | -| `variant` | see below | see below | -| `list` | create a typed array copy for number types; otherwise produce a JS array (like [`sequence`]) | same as [`sequence`] | -| `string` | same as [`USVString`] | same as [`USVString`] | -| `tuple` | TBD: maybe a [JS Tuple]? | TBD | -| `flags` | TBD: maybe a [JS Record]? | same as [`dictionary`] of optional `boolean` fields with default values of `false` | -| `enum` | same as [`enum`] | same as [`enum`] | -| `option` | same as [`T?`] | same as [`T?`] | -| `result` | same as `variant`, but coerce a top-level `error` return value to a thrown exception | same as `variant`, but coerce uncaught exceptions to top-level `error` return values | -| `map` | `new Map(_)` | `Map`s directly or other objects via `Object.entries(_)` | -| `own`, `borrow` | see below | see below | -| `future` | to a `Promise` | from a `Promise` | -| `stream` | to a `ReadableStream` | from a `ReadableStream` | - -Notes: -* Function parameter names are ignored since JavaScript doesn't have named - parameters. -* If a function's result type list is empty, the JavaScript function returns - `undefined`. If the result type list contains a single unnamed result, then - the return value is specified by `ToJSValue` above. Otherwise, the function - result is wrapped into a JS object whose field names are taken from the result - names and whose field values are specified by `ToJSValue` above. -* In lieu of an existing standard JS representation for `variant`, the JS API - would need to define its own custom binding built from objects. As a sketch, - the JS values accepted by `(variant (case "a" u32) (case "b" string))` could - include `{ tag: 'a', value: 42 }` and `{ tag: 'b', value: "hi" }`. -* For `option`, when Web IDL doesn't support particular type - combinations (e.g., `(option (option u32))`), the JS API would fall back to - the JS API of the unspecialized `variant` (e.g., - `(variant (case "some" (option u32)) (case "none"))`, despecializing only - the problematic outer `option`). -* When coercing `ToWebAssemblyValue`, `own` and `borrow` handle types would - dynamically guard that the incoming JS value's dynamic type was compatible - with the imported resource type referenced by the handle type. For example, - if a component contains `(import "Object" (type $Object (sub resource)))` and - is instantiated with the JS `Object` constructor, then `(own $Object)` and - `(borrow $Object)` could accept JS `object` values. -* When coercing `ToJSValue`, handle values would be wrapped with JS objects - that are instances of the handles' resource type's exported constructor - (described above). For `own` handles, a [`FinalizationRegistry`] would be - used to drop the `own` handle (thereby calling the resource destructor) when - its wrapper object was unreachable from JS. For `borrow` handles, the wrapper - object would become dynamically invalid (throwing on any access) at the end - of the export call. -* When an imported JavaScript function is a built-in function wrapping a Web - IDL function, the specified behavior should allow the intermediate JavaScript - call to be optimized away when the types are sufficiently compatible, falling - back to a plain call through JavaScript when the types are incompatible or - when the engine does not provide a separate optimized call path. - - -### ESM-integration - -Like the JS API, [ESM-integration] can be extended to load components in all -the same places where modules can be loaded today, branching on the `layer` -field in the binary format to determine whether to decode as a module or a -component. - -When present, the [`external-id`](#import-and-export-definitions) attribute of -an `import` would be used as the [Module Specifier], thereby giving components -the same naming expressivity as JavaScript (in particular, for importing URLs). -In the absence of an `external-id`, the always-present, but syntactically- -restrictive, `externname` of the import would be used instead. - -The main remaining question is how to deal with component imports having a -single string as well as the new importable component, module and instance -types. Going through these one by one: - -For component imports of module type, we need a new way to request that the ESM -loader parse or decode a module without *also* instantiating that module. -Recognizing this same need from JavaScript, there is a TC39 proposal called -[Import Reflection] that adds the ability to write, in JavaScript: -```js -import Foo from "./foo.wasm" as "wasm-module"; -assert(Foo instanceof WebAssembly.Module); -``` -With this extension to JavaScript and the ESM loader, a component import -of module type can be treated the same as `import ... as "wasm-module"`. - -Component imports of component type would work the same way as modules, -potentially replacing `"wasm-module"` with `"wasm-component"`. - -In all other cases, the (single) string imported by a component is first -resolved to a [Module Record] using the same process as resolving the -[Module Specifier] of a JavaScript `import`. After this, the handling of the -imported Module Record is determined by the import type: - -For imports of instance type, the ESM loader would treat the exports of the -instance type as if they were the [Named Imports] of a JavaScript `import`. -Thus, single-level imports of instance type act like the two-level imports -of Core WebAssembly modules where the first-level has been factored out. Since -the exports of an instance type can themselves be instance types, this process -must be performed recursively. - -Otherwise, function or value imports are treated like an [Imported Default Binding] -and the Module Record is converted to its default value. This allows the following -component: -```wat -;; bar.wasm -(component - (import "./foo.js" (func (result string))) - ... -) -``` -to be satisfied by a JavaScript module via ESM-integration: -```js -// foo.js -export default () => "hi"; -``` -when `bar.wasm` is loaded as an ESM: -```html - -``` - +This has been moved to [JS-Overview.md](JS-Overview.md) and [JS-Reference.md](JS-Reference.md). ## Examples diff --git a/design/mvp/JS-Explainer.md b/design/mvp/JS-Explainer.md new file mode 100644 index 00000000..a3a607e3 --- /dev/null +++ b/design/mvp/JS-Explainer.md @@ -0,0 +1,306 @@ +# WebAssembly Components JS-API Explainer + +This explainer describes how WebAssembly Components (hereafter 'components') can be used from JS. + +See the [reference](./JS-Reference.md) for an in-depth walkthrough. + +**This is a draft and is not complete.** + +## Walkthrough + +### Greeter: exporting a function + +Let's start with a component that imports nothing: + +```wat +(component + (export "greet" + (func (param "who" string) (result string)) + ) +) +``` + +```js +const { instance } = await WebAssembly.instantiate(bytes); + +instance.exports.greet("world"); // "hello, world" +``` + +`exports` holds one property per export and `greet` is an ordinary JS function. Component names are kebab-case and JS names are [camelCase](./JS-Reference.md#names), so an export named `greet-loudly` would be `greetLoudly`. + +Arguments are coerced to their expected type, and if that fails a `TypeError` is thrown: + +```js +instance.exports.greet(42); // "hello, 42" +instance.exports.greet(); // TypeError +``` + +Passing too few arguments is a `TypeError`. Extra arguments are ignored. + +### Logger: importing a function + +Now a component that imports: + +```wat +(component + (import "log" + (func (param "message" string)) + ) + (export "run" (func)) +) +``` + +The import `log` must be a JS callable object, and will be called with a JS String. + +We can provide the `console.log` builtin as here: + +```js +const imports = { log: console.log }; + +const { instance } = + await WebAssembly.instantiate(bytes, imports); + +instance.exports.run(); // logs "hello" +``` + +Or provide a custom implementation: + +```js +const lines = []; +const log = (message) => { lines.push(message); }; +const imports = { log }; + +const { instance } = + await WebAssembly.instantiate(bytes, imports); +``` + +### Values at a glance + +Components and JS maintain separate type/value systems, so any value crossing the boundary needs a defined translation in both directions. + +| Component type | JS | +|---|---| +| `bool` | Boolean | +| `s8`-`s32`, `u8`-`u32` | Number, an exact integer | +| `s64`, `u64` | BigInt | +| `f32`, `f64` | Number, including NaN and infinities | +| `char` | String of exactly one Unicode scalar value | +| `string` | String, well formed | +| `list` | `Uint8Array` | +| `list`, `list`, `tuple` | Array | +| `record { a-b: T }` | null-prototype object, `{ aB }` | +| `flags "a" "b"` | null-prototype object of Booleans, `{ a, b }` | +| `enum "a" "b"` | String, the label verbatim | +| `option` | `null`, or the payload | +| `variant`, `option>` | `{ kind, value }` | +| `result` | thrown and caught in return position, else `{ kind, value }` | +| `map` | `Map` | +| `own`, `borrow` | the original JS value for an imported resource type, an instance of its class for an exported one | +| `future`, `stream`, `error-context` | not yet specified | + +See [ToJSValue](./JS-Reference.md#tojsvalue) and [ToComponentValue](./JS-Reference.md#tocomponentvalue) for detailed algorithms. + +### Loading with ESM + +[ESM-integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) extends to components. The loader branches on the `layer` field of the binary, so a component loads anywhere a core module does today. + +Each component import becomes a JS import, and its module specifier is the import's [`external-id`](Explainer.md#import-and-export-definitions) if it has one and its name otherwise: + +```wat +(component + (import "slugify" + (external-id + "https://esm.unpkg.com/slugify@1.6.6") + (func (param "text" string) (result string)) + ) + (export "run" (func)) +) +``` + +```html + +``` + +### When a call fails + +Component functions signal failure using a `result` value: + 1. Exported component functions that return an error `result` throw JS exceptions. + 1. Imported JS functions that throw JS exceptions are captured as a `result`. + +An imported JS function that throws where the component asked for a plain return type results in a trap. + +```wat +(component + (import "lookup" + (func (param "key" string) (result string (error string))) + ) + (export "parse" + (func (param "text" string) (result u32 (error string))) + ) +) +``` + +`parse` tries to parse its `text` argument as an integer, and if that fails performs a fallible lookup. + +```js +const imports = { + lookup: (key) => { throw `no such key: ${key}`; }, +}; +const { instance } = + await WebAssembly.instantiate(bytes, imports); + +instance.exports.parse("42"); // 42 + +try { + instance.exports.parse("$name"); +} catch (e) { + e instanceof WebAssembly.ComponentError; // true + e.data; // "no such key: $name" +} +``` + +In the second call, parsing fails and leads to a call to `lookup` which throws a JS exception. This is converted to a `result` and consumed by the component. The component then propagates it to the original JS caller as a thrown `ComponentError` carrying the original message. + +### Importing a resource + +Components see JS values as resources. A resource type import is satisfied by passing a constructor function. + +Whenever a JS value must be converted to a resource type, an `instanceof` check is performed against the imported constructor. If the constructor is a [WebIDL interface object](https://webidl.spec.whatwg.org/#interface-object) or an [exported component resource constructor](#exporting-a-resource), a precise [brand check](./JS-Reference.md#brand-checks) is performed. + +Any imported function whose name is tagged `[constructor]`, `[method]`, or `[static]` is looked up on the imported constructor instead of the imports object: + +| name | import lookup | +|---|---| +| `[constructor]R` | `R` | +| `[method]R.M` | `R.prototype.M` | +| `[static]R.S` | `R.S` | + +The above allows most JS classes to be imported as a resource by just passing the constructor function: + +```wat +(component + (import "element" + (type $element (sub resource)) + ) + (import + "[method]element.query-selector" + (func + (param "self" (borrow $element)) + (param "selectors" string) + (result (option (own $element))) + ) + ) + (import "[method]element.get-attribute" + (func + (param "self" (borrow $element)) + (param "name" string) + (result (option string)) + ) + ) + (export "find" + (func + (param "root" (borrow $element)) + (param "selectors" string) + (result (option string)) + ) + ) +) +``` + +```js +const imports = { element: Element }; +const { instance } = + await WebAssembly.instantiate(bytes, imports); + +instance.exports.find(document.body, "h1"); // "page-title" or null +``` + +### Importing from the JS global + +The example above still needs someone to write `{ element: Element }`. A component can skip that and take its imports straight from the global object by importing `wasm:js/global`: + +```wat +(component + (import "wasm:js/global" + (instance $g + (export "btoa" + (func (param "data" string) (result string)) + ) + + (export "element" (type $element (sub resource))) + (export "[method]element.get-attribute" + (func + (param "self" (borrow $element)) + (param "name" string) + (result (option string)) + ) + ) + ) + ) + (alias export $g "element" (type $el)) + + (export "encode-id" + (func + (param "el" (borrow $el)) + (result (option string)) + ) + ) +) +``` + +```js +const exports = + await WebAssembly.instantiate(bytes, { builtins: ["js/global"] }).exports; + +exports.encodeId(document.body); +``` + +Importing from `wasm:js/global` is equivalent to an imports object with: `{ "wasm:js/global": globalThis }`. The normal rules for reading from the imports object still apply. + +ESM-integration defaults to enabling `wasm:js/global` which allows a component to import and use web APIs without any glue code: + +```html + +``` + +### Exporting a resource + +A resource type exported from a component becomes a JS class: + +```wat +(component + (export "counter" (type $counter (sub resource))) + (export "[constructor]counter" + (func (result (own $counter))) + ) + (export "[method]counter.increment" + (func + (param "self" (borrow $counter)) + (result u32) + ) + ) +) +``` + +```js +const { instance } = await WebAssembly.instantiate(bytes); +const { Counter } = instance.exports; + +let c = new Counter(); +c.increment(); // 1 +c.increment(); // 2 +``` + +## Status + +- `async` functions, `future`, `stream` and `error-context` have no binding yet. + +Everything else we know to be open is collected in the reference's [follow ups](./JS-Reference.md#follow-ups). diff --git a/design/mvp/JS-Reference.md b/design/mvp/JS-Reference.md new file mode 100644 index 00000000..4b3ba405 --- /dev/null +++ b/design/mvp/JS-Reference.md @@ -0,0 +1,986 @@ +# WebAssembly Components JS-API Reference + +This is the in-depth reference for the WebAssembly Component JS-API. See the [explainer](./JS-Explainer.md) for a higher-level introduction. + +**This is a draft and is not complete. Major details are unresolved, and there are bugs.** + +## Goals + +1. Components can import and use most web and JS APIs +2. Components can export an API usable by JS +3. Components interact with the web platform in similar ways to JS: + 1. Components can feature test whether APIs are present + 1. Components work whether they are importing a web API, or a JS polyfill, or a component polyfill + 1. Components are tolerant of web API evolution + 1. Component misuse of a web API results in failure at that call-site, not a link time error +4. Components have improved performance when calling web APIs compared to today + +## Non-goals + +1. Components importing every kind of web API +1. Components exporting any kind of JS API + +## The WebAssembly namespace + +We extend the imperative WebAssembly JS-API interfaces to also allow validation, compilation, and instantiation of components in addition to modules. + +```webidl +[LegacyNamespace=WebAssembly, Exposed=*] +interface Component { + constructor([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); +}; + +[LegacyNamespace=WebAssembly, Exposed=*] +interface ComponentInstance { + constructor(Component component, optional object importsObject); + readonly attribute object exports; +}; + +typedef (Component or Module) InstantiateSource; + +dictionary WebAssemblyInstantiatedComponentSource { + required Component component; + required ComponentInstance instance; +}; + +[Exposed=*] +namespace WebAssembly { + // Same as before, but now will detect if the bytes are a component or module and dispatch differently. + boolean validate([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); + Promise compile([AllowResizable] AllowSharedBufferSource bytes, optional WebAssemblyCompileOptions options = {}); + Promise<(WebAssemblyInstantiatedSource or WebAssemblyInstantiatedComponentSource)> instantiate( + [AllowResizable] AllowSharedBufferSource bytes, optional object importObject, optional WebAssemblyCompileOptions options = {}); + + // Now takes an InstantiateSource instead of just a Module, and returns a + // ComponentInstance for a Component. + Promise<(Instance or ComponentInstance)> instantiate( + InstantiateSource moduleObject, optional object importObject); +}; +``` + +We also add an error type for component functions that return `result<_, E>` to JS: + +```webidl +[LegacyNamespace=WebAssembly, Exposed=*] +interface ComponentError : Error { + constructor(optional DOMString message = "", optional any data); + readonly attribute any data; +}; +``` + +`data` is the converted `E` value. See [Create the exports object](#create-the-exports-object). + +## Validation/compilation + +Validation and compilation of components defer to the underlying component embedding interface. This reference adds nothing to it. + +## Entry points + +A `Component` has the following slots: + 1. [[Component]] - the compiled component. + 1. [[EnabledBuiltins]] - the enabled builtins. + +A `ComponentInstance` has the following slots: + 1. [[ComponentInstance]] - the component instance. + 1. [[Exports]] - the [exports object](#create-the-exports-object). + 1. [[HostResourceTypes]] - map from [abstract type key](#abstract-and-transparent-types) to [host resource type](#host-resource-types-and-values). + 1. [[GuestResourceClasses]] - map from [abstract type key](#abstract-and-transparent-types) to [guest resource class](#guest-resource-classes). + +To `construct a Component` given |bytes| and |options|: +1. Let |stableBytes| be a copy of the bytes held by |bytes|. +1. Let |enabledBuiltins| be the result of parsing [`builtins`](#builtin-imports) from |options|. +1. Let |component| be the result of compiling |stableBytes| as a component, per the embedding interface. +1. If compilation fails: + 1. Throw a `WebAssembly.CompileError`. +1. Set **this**.[[Component]] to |component|. +1. Set **this**.[[EnabledBuiltins]] to |enabledBuiltins|. + +To `construct a ComponentInstance` given a `Component` |component|, and |importsObject|: +1. Let |enabledBuiltins| be |component|.[[EnabledBuiltins]]. +1. Let |result| be ? [`instantiate a component from an imports object`](#instantiation) given |component|.[[Component]], |importsObject|, |enabledBuiltins|. +1. Set **this**.[[ComponentInstance]] to |result|.[[ComponentInstance]]. +1. Set **this**.[[Exports]] to |result|.[[Exports]]. +1. Set **this**.[[HostResourceTypes]] to |result|.[[HostResourceTypes]]. +1. Set **this**.[[GuestResourceClasses]] to |result|.[[GuestResourceClasses]]. + +The `exports` getter returns **this**.[[Exports]]. + +`validate` is modified to validate the bytes as a component if the `layer` field of the binary indicates a component. +`compile`/`instantiate` are modified to asynchronously compile/instantiate the bytes as a component if the `layer` field indicates a component. + +## Names + +Component import/export `plainname`s contain [`label`s](Explainer.md#import-and-export-definitions) that must be transformed into an identifier for use with JS. + +Component import/export `interfacename`s (such as `wasi:http/handler@1.0.0`) are used as-is when converted to JS strings or as [module specifiers](#webassembly-esm-integration). + +We define `PascalCase(label)` and `CamelCase(label)` below. + +| `label` | `PascalCase` | `CamelCase` | +|---|---|---| +| `element` | `Element` | `element` | +| `query-selector` | `QuerySelector` | `querySelector` | +| `inner-HTML` | `InnerHTML` | `innerHTML` | +| `XML-http-request` | `XMLHttpRequest` | `xmlHttpRequest` | +| `URL` | `URL` | `url` | +| `a1-2-3` | `A123` | `a123` | + +Every `plainname` matches exactly one of the four patterns below, and no `interfacename` matches any of them. The algorithms in this document dispatch on these patterns and read their named captures. `