Skip to content

Commit 767e88c

Browse files
guybedfordtargos
authored andcommitted
esm: unwrap WebAssembly.Global on Wasm Namespaces
PR-URL: #57525 Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 8bc0452 commit 767e88c

File tree

8 files changed

+499
-15
lines changed

8 files changed

+499
-15
lines changed

lib/internal/modules/esm/create_dynamic_module.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import.meta.exports[${nameStringLit}] = {
4444
* @param {string} [url=''] - The URL of the module.
4545
* @param {(reflect: DynamicModuleReflect) => void} evaluate - The function to evaluate the module.
4646
* @typedef {object} DynamicModuleReflect
47-
* @property {string[]} imports - The imports of the module.
47+
* @property {Record<string, Record<string, any>>} imports - The imports of the module.
4848
* @property {string[]} exports - The exports of the module.
4949
* @property {(cb: (reflect: DynamicModuleReflect) => void) => void} onReady - Callback to evaluate the module.
5050
*/

lib/internal/modules/esm/translators.js

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
'use strict';
22

33
const {
4-
ArrayPrototypeMap,
54
ArrayPrototypePush,
65
FunctionPrototypeCall,
76
JSONParse,
8-
ObjectKeys,
7+
ObjectAssign,
98
ObjectPrototypeHasOwnProperty,
109
ReflectApply,
1110
SafeArrayIterator,
1211
SafeMap,
1312
SafeSet,
13+
SafeWeakMap,
1414
StringPrototypeIncludes,
1515
StringPrototypeReplaceAll,
1616
StringPrototypeSlice,
@@ -484,6 +484,14 @@ translators.set('json', function jsonStrategy(url, source) {
484484
});
485485

486486
// Strategy for loading a wasm module
487+
// This logic should collapse into WebAssembly Module Record in future.
488+
/**
489+
* @type {WeakMap<
490+
* import('internal/modules/esm/utils').ModuleNamespaceObject,
491+
* WebAssembly.Instance
492+
* >} [[Instance]] slot proxy for WebAssembly Module Record
493+
*/
494+
const wasmInstances = new SafeWeakMap();
487495
translators.set('wasm', async function(url, source) {
488496
emitExperimentalWarning('Importing WebAssembly modules');
489497

@@ -502,19 +510,61 @@ translators.set('wasm', async function(url, source) {
502510
throw err;
503511
}
504512

505-
const imports =
506-
ArrayPrototypeMap(WebAssembly.Module.imports(compiled),
507-
({ module }) => module);
508-
const exports =
509-
ArrayPrototypeMap(WebAssembly.Module.exports(compiled),
510-
({ name }) => name);
513+
const importsList = new SafeSet();
514+
const wasmGlobalImports = [];
515+
for (const impt of WebAssembly.Module.imports(compiled)) {
516+
if (impt.kind === 'global') {
517+
ArrayPrototypePush(wasmGlobalImports, impt);
518+
}
519+
importsList.add(impt.module);
520+
}
521+
522+
const exportsList = new SafeSet();
523+
const wasmGlobalExports = new SafeSet();
524+
for (const expt of WebAssembly.Module.exports(compiled)) {
525+
if (expt.kind === 'global') {
526+
wasmGlobalExports.add(expt.name);
527+
}
528+
exportsList.add(expt.name);
529+
}
511530

512-
const createDynamicModule = require(
513-
'internal/modules/esm/create_dynamic_module');
514-
const { module } = createDynamicModule(imports, exports, url, (reflect) => {
531+
const createDynamicModule = require('internal/modules/esm/create_dynamic_module');
532+
533+
const { module } = createDynamicModule([...importsList], [...exportsList], url, (reflect) => {
534+
for (const impt of importsList) {
535+
const importNs = reflect.imports[impt];
536+
const wasmInstance = wasmInstances.get(importNs);
537+
if (wasmInstance) {
538+
const wrappedModule = ObjectAssign({ __proto__: null }, reflect.imports[impt]);
539+
for (const { module, name } of wasmGlobalImports) {
540+
if (module !== impt) {
541+
continue;
542+
}
543+
// Import of Wasm module global -> get direct WebAssembly.Global wrapped value.
544+
// JS API validations otherwise remain the same.
545+
wrappedModule[name] = wasmInstance[name];
546+
}
547+
reflect.imports[impt] = wrappedModule;
548+
}
549+
}
550+
// In cycles importing unexecuted Wasm, wasmInstance will be undefined, which will fail during
551+
// instantiation, since all bindings will be in the Temporal Deadzone (TDZ).
515552
const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
516-
for (const expt of ObjectKeys(exports)) {
517-
reflect.exports[expt].set(exports[expt]);
553+
wasmInstances.set(module.getNamespace(), exports);
554+
for (const expt of exportsList) {
555+
let val = exports[expt];
556+
// Unwrap WebAssembly.Global for JS bindings
557+
if (wasmGlobalExports.has(expt)) {
558+
try {
559+
// v128 will throw in GetGlobalValue, see:
560+
// https://webassembly.github.io/esm-integration/js-api/index.html#getglobalvalue
561+
val = val.value;
562+
} catch {
563+
// v128 doesn't support ToJsValue() -> use undefined (ideally should stay in TDZ)
564+
continue;
565+
}
566+
}
567+
reflect.exports[expt].set(val);
518568
}
519569
});
520570
// WebAssembly modules support source phase imports, to import the compiled module

test/es-module/test-esm-wasm.mjs

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,154 @@ describe('ESM: WASM modules', { concurrency: !process.env.TEST_PARALLEL }, () =>
5252
[
5353
'import { strictEqual } from "node:assert";',
5454
`import * as wasmExports from ${JSON.stringify(fixtures.fileURL('es-modules/export-name-syntax-error.wasm'))};`,
55-
'assert.strictEqual(wasmExports["?f!o:o<b>a[r]"]?.value, 12682);',
55+
'assert.strictEqual(wasmExports["?f!o:o<b>a[r]"], 12682);',
56+
].join('\n'),
57+
]);
58+
59+
strictEqual(stderr, '');
60+
strictEqual(stdout, '');
61+
strictEqual(code, 0);
62+
});
63+
64+
it('should properly handle all WebAssembly global types', async () => {
65+
const { code, stderr, stdout } = await spawnPromisified(execPath, [
66+
'--no-warnings',
67+
'--experimental-wasm-modules',
68+
'--input-type=module',
69+
'--eval',
70+
[
71+
'import { strictEqual, deepStrictEqual, ok } from "node:assert";',
72+
73+
// SIMD is not supported on rhel8-ppc64le
74+
'let wasmExports;',
75+
'try {',
76+
` wasmExports = await import(${JSON.stringify(fixtures.fileURL('es-modules/globals.wasm'))});`,
77+
'} catch (e) {',
78+
' ok(e instanceof WebAssembly.CompileError);',
79+
' ok(e.message.includes("SIMD unsupported"));',
80+
'}',
81+
82+
'if (wasmExports) {',
83+
84+
// Test imported globals using direct access
85+
' strictEqual(wasmExports.importedI32, 42);',
86+
' strictEqual(wasmExports.importedMutI32, 100);',
87+
' strictEqual(wasmExports.importedI64, 9223372036854775807n);',
88+
' strictEqual(wasmExports.importedMutI64, 200n);',
89+
' strictEqual(Math.round(wasmExports.importedF32 * 100000) / 100000, 3.14159);',
90+
' strictEqual(Math.round(wasmExports.importedMutF32 * 100000) / 100000, 2.71828);',
91+
' strictEqual(wasmExports.importedF64, 3.141592653589793);',
92+
' strictEqual(wasmExports.importedMutF64, 2.718281828459045);',
93+
' strictEqual(wasmExports.importedExternref !== null, true);',
94+
' strictEqual(wasmExports.importedMutExternref !== null, true);',
95+
' strictEqual(wasmExports.importedNullExternref, null);',
96+
97+
// Test local globals exported directly
98+
' strictEqual(wasmExports[\'🚀localI32\'], 42);',
99+
' strictEqual(wasmExports.localMutI32, 100);',
100+
' strictEqual(wasmExports.localI64, 9223372036854775807n);',
101+
' strictEqual(wasmExports.localMutI64, 200n);',
102+
' strictEqual(Math.round(wasmExports.localF32 * 100000) / 100000, 3.14159);',
103+
' strictEqual(Math.round(wasmExports.localMutF32 * 100000) / 100000, 2.71828);',
104+
' strictEqual(wasmExports.localF64, 2.718281828459045);',
105+
' strictEqual(wasmExports.localMutF64, 3.141592653589793);',
106+
107+
// Test imported globals using getter functions
108+
' strictEqual(wasmExports.getImportedMutI32(), 100);',
109+
' strictEqual(wasmExports.getImportedMutI64(), 200n);',
110+
' strictEqual(Math.round(wasmExports.getImportedMutF32() * 100000) / 100000, 2.71828);',
111+
' strictEqual(wasmExports.getImportedMutF64(), 2.718281828459045);',
112+
' strictEqual(wasmExports.getImportedMutExternref() !== null, true);',
113+
114+
// Test local globals using getter functions
115+
' strictEqual(wasmExports.getLocalMutI32(), 100);',
116+
' strictEqual(wasmExports.getLocalMutI64(), 200n);',
117+
' strictEqual(Math.round(wasmExports.getLocalMutF32() * 100000) / 100000, 2.71828);',
118+
' strictEqual(wasmExports.getLocalMutF64(), 3.141592653589793);',
119+
' strictEqual(wasmExports.getLocalMutExternref(), null);',
120+
121+
' assert.throws(wasmExports.getLocalMutV128);',
122+
123+
// Pending TDZ support
124+
' strictEqual(wasmExports.depV128, undefined);',
125+
126+
// Test modifying mutable globals and reading the new values
127+
' wasmExports.setImportedMutI32(999);',
128+
' strictEqual(wasmExports.getImportedMutI32(), 999);',
129+
130+
' wasmExports.setImportedMutI64(888n);',
131+
' strictEqual(wasmExports.getImportedMutI64(), 888n);',
132+
133+
' wasmExports.setImportedMutF32(7.77);',
134+
' strictEqual(Math.round(wasmExports.getImportedMutF32() * 100) / 100, 7.77);',
135+
136+
' wasmExports.setImportedMutF64(6.66);',
137+
' strictEqual(wasmExports.getImportedMutF64(), 6.66);',
138+
139+
// Test modifying mutable externref
140+
' const testObj = { test: "object" };',
141+
' wasmExports.setImportedMutExternref(testObj);',
142+
' strictEqual(wasmExports.getImportedMutExternref(), testObj);',
143+
144+
// Test modifying local mutable globals
145+
' wasmExports.setLocalMutI32(555);',
146+
' strictEqual(wasmExports.getLocalMutI32(), 555);',
147+
148+
' wasmExports.setLocalMutI64(444n);',
149+
' strictEqual(wasmExports.getLocalMutI64(), 444n);',
150+
151+
' wasmExports.setLocalMutF32(3.33);',
152+
' strictEqual(Math.round(wasmExports.getLocalMutF32() * 100) / 100, 3.33);',
153+
154+
' wasmExports.setLocalMutF64(2.22);',
155+
' strictEqual(wasmExports.getLocalMutF64(), 2.22);',
156+
157+
// These mutating pending live bindings support
158+
' strictEqual(wasmExports.localMutI32, 100);',
159+
' strictEqual(wasmExports.localMutI64, 200n);',
160+
' strictEqual(Math.round(wasmExports.localMutF32 * 100) / 100, 2.72);',
161+
' strictEqual(wasmExports.localMutF64, 3.141592653589793);',
162+
163+
// Test modifying local mutable externref
164+
' const anotherTestObj = { another: "test object" };',
165+
' wasmExports.setLocalMutExternref(anotherTestObj);',
166+
' strictEqual(wasmExports.getLocalMutExternref(), anotherTestObj);',
167+
' strictEqual(wasmExports.localMutExternref, null);',
168+
169+
// Test dep.wasm imports
170+
' strictEqual(wasmExports.depI32, 1001);',
171+
' strictEqual(wasmExports.depMutI32, 2001);',
172+
' strictEqual(wasmExports.getDepMutI32(), 2001);',
173+
' strictEqual(wasmExports.depI64, 10000000001n);',
174+
' strictEqual(wasmExports.depMutI64, 20000000001n);',
175+
' strictEqual(wasmExports.getDepMutI64(), 20000000001n);',
176+
' strictEqual(Math.round(wasmExports.depF32 * 100) / 100, 10.01);',
177+
' strictEqual(Math.round(wasmExports.depMutF32 * 100) / 100, 20.01);',
178+
' strictEqual(Math.round(wasmExports.getDepMutF32() * 100) / 100, 20.01);',
179+
' strictEqual(wasmExports.depF64, 100.0001);',
180+
' strictEqual(wasmExports.depMutF64, 200.0001);',
181+
' strictEqual(wasmExports.getDepMutF64(), 200.0001);',
182+
183+
// Test modifying dep.wasm mutable globals
184+
' wasmExports.setDepMutI32(3001);',
185+
' strictEqual(wasmExports.getDepMutI32(), 3001);',
186+
187+
' wasmExports.setDepMutI64(30000000001n);',
188+
' strictEqual(wasmExports.getDepMutI64(), 30000000001n);',
189+
190+
' wasmExports.setDepMutF32(30.01);',
191+
' strictEqual(Math.round(wasmExports.getDepMutF32() * 100) / 100, 30.01);',
192+
193+
' wasmExports.setDepMutF64(300.0001);',
194+
' strictEqual(wasmExports.getDepMutF64(), 300.0001);',
195+
196+
// These pending live bindings support
197+
' strictEqual(wasmExports.depMutI32, 2001);',
198+
' strictEqual(wasmExports.depMutI64, 20000000001n);',
199+
' strictEqual(Math.round(wasmExports.depMutF32 * 100) / 100, 20.01);',
200+
' strictEqual(wasmExports.depMutF64, 200.0001);',
201+
202+
'}',
56203
].join('\n'),
57204
]);
58205

test/fixtures/es-modules/dep.wasm

529 Bytes
Binary file not shown.

test/fixtures/es-modules/dep.wat

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
(module
2+
(type (;0;) (func (result i32)))
3+
(type (;1;) (func (result i64)))
4+
(type (;2;) (func (result f32)))
5+
(type (;3;) (func (result f64)))
6+
(type (;4;) (func (result externref)))
7+
(type (;5;) (func (result v128)))
8+
(global $i32_value (;0;) i32 i32.const 1001)
9+
(global $i32_mut_value (;1;) (mut i32) i32.const 2001)
10+
(global $i64_value (;2;) i64 i64.const 10000000001)
11+
(global $i64_mut_value (;3;) (mut i64) i64.const 20000000001)
12+
(global $f32_value (;4;) f32 f32.const 0x1.4051ecp+3 (;=10.01;))
13+
(global $f32_mut_value (;5;) (mut f32) f32.const 0x1.4028f6p+4 (;=20.01;))
14+
(global $f64_value (;6;) f64 f64.const 0x1.90001a36e2eb2p+6 (;=100.0001;))
15+
(global $f64_mut_value (;7;) (mut f64) f64.const 0x1.90000d1b71759p+7 (;=200.0001;))
16+
(global $externref_value (;8;) externref ref.null extern)
17+
(global $externref_mut_value (;9;) (mut externref) ref.null extern)
18+
(global $v128_value (;10;) v128 v128.const i32x4 0x0000000a 0x00000014 0x0000001e 0x00000028)
19+
(global $v128_mut_value (;11;) (mut v128) v128.const i32x4 0x00000032 0x0000003c 0x00000046 0x00000050)
20+
(export "i32_value" (global $i32_value))
21+
(export "i32_mut_value" (global $i32_mut_value))
22+
(export "i64_value" (global $i64_value))
23+
(export "i64_mut_value" (global $i64_mut_value))
24+
(export "f32_value" (global $f32_value))
25+
(export "f32_mut_value" (global $f32_mut_value))
26+
(export "f64_value" (global $f64_value))
27+
(export "f64_mut_value" (global $f64_mut_value))
28+
(export "externref_value" (global $externref_value))
29+
(export "externref_mut_value" (global $externref_mut_value))
30+
(export "v128_value" (global $v128_value))
31+
(export "v128_mut_value" (global $v128_mut_value))
32+
)

test/fixtures/es-modules/globals.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// globals.js - Direct global exports for WebAssembly imports
2+
3+
// Immutable globals (simple values)
4+
const i32_value = 42;
5+
export { i32_value as '🚀i32_value' }
6+
export const i64_value = 9223372036854775807n; // Max i64 value
7+
export const f32_value = 3.14159;
8+
export const f64_value = 3.141592653589793;
9+
10+
// Mutable globals with WebAssembly.Global wrapper
11+
export const i32_mut_value = new WebAssembly.Global({ value: 'i32', mutable: true }, 100);
12+
export const i64_mut_value = new WebAssembly.Global({ value: 'i64', mutable: true }, 200n);
13+
export const f32_mut_value = new WebAssembly.Global({ value: 'f32', mutable: true }, 2.71828);
14+
export const f64_mut_value = new WebAssembly.Global({ value: 'f64', mutable: true }, 2.718281828459045);
15+
16+
export const externref_value = { hello: 'world' };
17+
export const externref_mut_value = new WebAssembly.Global({ value: 'externref', mutable: true }, { mutable: 'global' });
18+
export const null_externref_value = null;

test/fixtures/es-modules/globals.wasm

3.26 KB
Binary file not shown.

0 commit comments

Comments
 (0)