Skip to content

Support direct global bindings #104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 76 additions & 16 deletions document/js-api/index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ urlPrefix: https://webassembly.github.io/spec/core/; spec: WebAssembly; type: df
text: memory address; url: exec/runtime.html#syntax-memaddr
text: global address; url: exec/runtime.html#syntax-globaladdr
text: extern address; url: exec/runtime.html#syntax-externaddr
text: extern subtype; url: valid/types.html#match-externtype
text: page size; url: exec/runtime.html#page-size
url: syntax/types.html#syntax-numtype
text: i32
Expand Down Expand Up @@ -1391,6 +1392,8 @@ To <dfn export>parse a WebAssembly module</dfn> given a <a>byte sequence</a> |by
1. For each (|moduleName|, <var ignore>name</var>, <var ignore>type</var>) in [=module_imports=](|module|.\[[Module]]),
1. [=set/Append=] |moduleName| to |requestedModules|.
1. Let |moduleRecord| be {
<!-- WebAssembly Module Records -->
\[[Instance]]: ~empty~,
<!-- Abstract Module Records -->
\[[Realm]]: |realm|,
\[[Environment]]: ~empty~,
Expand Down Expand Up @@ -1470,25 +1473,80 @@ WebAssembly Module Records have the following methods:
<h3 id="module-execution">ExecuteModule ( [ |promiseCapability| ] ) Concrete Method</h3>
1. Assert: |promiseCapability| was not provided.
1. Let |record| be this WebAssembly Module Record.
1. Let |module| be |record|.\[[ModuleSource]].
1. Let |imports| be a new, empty [=map=].
1. For each (|importedModuleName|, |name|, <var ignore>type</var>) in [=module_imports=](|module|.\[[Module]]),
1. If |imports|[|importedModuleName|] does not exist, set |imports|[|importedModuleName|] to a new, empty [=map=].
1. Let |module| be |record|.\[[ModuleSource]].\[[Module]].
1. Let |imports| be « ».
1. [=list/iterate|For each=] (|importedModuleName|, |name|, |importtype|) in [=module_imports=](|module|),
1. Let |importedModule| be [$GetImportedModule$](|record|, |importedModuleName|).
1. Let |value| be ? |importedModule|.\[[Environment]].GetBindingValue(|name|, true).
1. Set |imports|[|importedModuleName|][|name|] to |value|.
1. Let |importObject| be ! [$OrdinaryObjectCreate$](null).
1. For each |key| → |value| of |imports|,
1. Let |moduleImportsObject| be ! [$OrdinaryObjectCreate$](null).
1. For each |importedName| → |importedValue| of |value|,
1. Perform ! [$CreateDataPropertyOrThrow$](|moduleImportsObject|, |importedName|, |importedValue|).
1. Perform ! [$CreateDataPropertyOrThrow$](|importObject|, |key|, |moduleImportsObject|).
1. [=Read the imports=] of |module| with imports |importObject|, and let |imports| be the result.
1. Let |resolution| be |importedModule|.ResolveExport(|name|).
1. If |resolution|.\[[Module]] is a WebAssembly Module Record,
1. Let |resolutionInstance| be |resolution|.\[[Module]].\[[Instance]].
1. If |resolutionInstance| is ~empty~ then,
1. Throw a {LinkError} exception.
1. Let |resolutionModule| be |resolution|.\[[Module]].\[[ModuleSource]].\[[Module]].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this is confusing, I thought [[Module]] was the Module Record corresponding to [[ModuleSource]] rather than a WebAssembly module, but I don't think we can improve it anyway)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's unfortunate we have [[Module]] on both a resolve binding record and a WebAssembly.Module module object. Perhaps resolve binding records could refactor at some point to use [[ResolvedModule]] or something like that to ensure clarity in this kind of scenario.

I've split out resolutionModule and resolutionName and also added some assertions to make the relationships clearer.

1. Let |resolutionName| be |resolution|.\[[BindingName]].
1. Let |externval| be [=instance_export=](|resolutionInstance|, |resolutionName|).
1. Assert: |externval| is not [=error=].
1. Assert: [=module_exports=](|resolutionModule|) contains an element (|resolutionName|, <var ignore>type</var>).
1. Let |externtype| be the value of |type| for the element (|resolutionName|, |type|) in [=module_exports=](|resolutionModule|).
1. If |importtype| is not an [=extern subtype=] of |externtype|, throw a {{LinkError}} exception.
1. [=list/Append=] |externval| to |imports|.
1. Otherwise,
1. Let |env| be |resolution|.\[[Module]].\[[Environment]].
1. Let |v| be [=?=] |env|.GetBindingValue(|resolution|.\[[BindingName]], true).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if [[BindingName]] is NAMESPACE? e.g. if I import foo from a module that does export * as foo from "./bar.js".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are reading directly from the environment record here, so NAMESPACE cannot be a value in an environment record in ECMA-262, you'd get the namespace object representation instead.

1. If |importtype| is of the form [=func=] |functype|,
1. If [$IsCallable$](|v|) is false, throw a {{LinkError}} exception.
1. If |v| has a \[[FunctionAddress]] internal slot, and therefore is an [=Exported Function=],
1. Let |funcaddr| be the value of |v|'s \[[FunctionAddress]] internal slot.
1. Otherwise,
1. [=Create a host function=] from |v| and |functype|, and let |funcaddr| be the result.
1. Let <var ignore>index</var> be the number of external functions in |imports|, defining the [=index of the host function=] |funcaddr|.
1. Let |externfunc| be the [=external value=] [=external value|func=] |funcaddr|.
1. [=list/Append=] |externfunc| to |imports|.
1. If |importtype| is of the form [=global=] |mut| |valtype|,
1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. If |v| [=implements=] {{Global}},
1. Let |globaladdr| be |v|.\[[Global]].
1. Let |targetmut| <var ignore>valuetype</var> be [=global_type=](|store|, |globaladdr|).
1. If |mut| is [=const=] and |targetmut| is [=var=], throw a {{LinkError}} exception.
1. Otherwise,
1. If |valtype| is [=v128=], throw a {{LinkError}} exception.
1. If |mut| is [=var=], throw a {{LinkError}} exception.
1. Let |value| be [=?=] [=ToWebAssemblyValue=](|v|, |valtype|).
1. Let (|store|, |globaladdr|) be [=global_alloc=](|store|, |mut| |valtype|, |value|).
1. Set the [=surrounding agent=]'s [=associated store=] to |store|.
1. Let |externglobal| be [=external value|global=] |globaladdr|.
1. [=list/Append=] |externglobal| to |imports|.
1. If |importtype| is of the form [=mem=] <var ignore>memtype</var>,
1. If |v| does not [=implement=] {{Memory}}, throw a {{LinkError}} exception.
1. Let |externmem| be the [=external value=] [=external value|mem=] |v|.\[[Memory]].
1. [=list/Append=] |externmem| to |imports|.
1. If |importtype| is of the form [=table=] <var ignore>tabletype</var>,
1. If |v| does not [=implement=] {{Table}}, throw a {{LinkError}} exception.
1. Let |tableaddr| be |v|.\[[Table]].
1. Let |externtable| be the [=external value=] [=external value|table=] |tableaddr|.
1. [=list/Append=] |externtable| to |imports|.
1. [=Instantiate the core of a WebAssembly module=] |module| with |imports|, and let |instance| be the result.
1. For each |name| in the [=export name list=] of |record|,
1. Perform ! |record|.\[[Environment]].InitializeBinding(|name|, ! Get(|instance|.\[[Exports]], |name|)).
1. Set |record|.\[[Instance]] to |instance|.
1. [=list/iterate|For each=] (|name|, |externtype|) of [=module_exports=](|module|),
1. If |externtype| is of the form [=global=] |mut| |globaltype|,
1. Assert: |externval| is of the form [=external value|global=] |globaladdr|.
1. Let [=external value|global=] |globaladdr| be |externval|.
1. Let |global_value| be [=global_read=](|store|, |globaladdr|).
1. If |globaltype| is not [=v128=],
1. Note: The condition above leaves unsupported JS values as uninitialized in TDZ and therefore as a reference error on
access. When integrating with shared globals, they may be excluded here similarly to v128 above.
Comment on lines +1551 to +1552
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we do this instead of simply not exposing them, making it a linking error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great point and thanks for spotting this. The reason is that we need these bindings to work when Wasm modules import other Wasm modules. That is, while these values are not expressible in JS, they should still be exposed as exports to other Wasm. TDZ is therefore a useful way to represent this property.

1. Perform [=!=] |record|.\[[Environment]].InitializeBinding(|name|, [=ToJSValue=](|global_value|)).
1. If |mut| is [=var=], then associate all future mutations of |globaladdr| with the ECMA-262 binding record for |name| in
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|record|.\[[Environment]], such that |record|.\[[Environment]].GetBindingValue(|resolution|.\[[BindingName]], true)
always returns [=ToJSValue=]([=global_read=](|store|, |globaladdr|)) for the current [=surrounding agent=]'s
[=associated store=] |store|.
1. Otherwise,
1. Perform ! |record|.\[[Environment]].InitializeBinding(|name|, ! Get(|instance|.\[[Exports]], |name|)).

Note: exported bindings are left uninitialized, i.e., in TDZ.
Note: The linking semantics here for Wasm to Wasm modules are identical to the WebAssembly JS API semantics as if passing the
the exports object as the imports object in instantiation. When linking Wasm module imports to JS module exports, the JS API semantics
are exactly followed as well. It is only in the case of importing Wasm from JS that WebAssembly.Global unwrapping is observable on the
WebAssembly Module Record Environment Record.

</div>

Expand All @@ -1500,6 +1558,8 @@ WebAssembly Module Records have the following methods:
1. For each (|moduleName|, <var ignore>name</var>, <var ignore>type</var>) in [=module_imports=](|specifier|.\[[Module]]),
1. [=set/Append=] |moduleName| to |requestedModules|.
1. Let |moduleRecord| be {
<!-- WebAssembly Module Records -->
\[[Instance]]: ~empty~,
<!-- Abstract Module Records -->
\[[Realm]]: |realm|,
\[[Environment]]: ~empty~,
Expand Down
17 changes: 13 additions & 4 deletions proposals/esm-integration/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ export const count = new WebAssembly.Global({
}, 42);
```

Note that counter.js could equivalently be written:

```js
// counter.js
export let count = 42;
```

Which would still be picked up as a global import value, although changes to the JS value would not affect the WebAssembly global value (live bindings are only supported for Wasm module exports).

##### External type imports

```wasm
Expand Down Expand Up @@ -109,7 +118,7 @@ for (let index = 0; index < length; index++)

| export type | imported value |
|-------------|---------------------------|
| global | `WebAssembly.Global` object |
| global | The corresponding JS value for the global |
| memory | `WebAssembly.Memory` object |
| table | `WebAssembly.Table` object |
| function | WebAssembly exported function |
Expand All @@ -123,16 +132,16 @@ Wasm bindings cannot be reassigned as it can in JS, so the exported value will n
1. wasm module is instantiated evaluated. Functions are initialized. Memories and tables are initialized and filled with data/elem sections. Globals are initialized and initializer expressions are evaluated. The start function runs.
1. JS module is evaluated. All values are available.

Currently, the value of the export for something like `WebAssembly.Global` would be accessed using the `.value` property on the JS object. However, when host bindings are in place, these could be annotated with a host binding that turns it into a real live binding that points directly to the value's address.
The value of the export for `WebAssembly.Global` is provided directly on the JS namespace object, with mutable globals reflected as live bindings to JS.

#### Example

```js
// main.js
import {count, increment} from "./counter.wasm";
console.log(count.value); // logs 5
console.log(count); // logs 5
increment();
console.log(count.value); // logs 6
console.log(count); // logs 6
```
```wasm
;; counter.wat --> counter.wasm
Expand Down
3 changes: 1 addition & 2 deletions proposals/esm-integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,4 @@ If custom compilation options are needed or if custom streams need to be provide

### Where is the specification for this proposal?

If you want to dig into the details, see [the updated WebAssembly JS API](https://webassembly.github.io/esm-integration/js-api/index.html#esm-integration) and [the proposed HTML integration PR](https://github.com/whatwg/html/pull/4372).

If you want to dig into the details, see [the updated WebAssembly JS API](https://webassembly.github.io/esm-integration/js-api/index.html#esm-integration) and [th HTML integration PR](https://github.com/whatwg/html/pull/10380).