Skip to content

Commit 0f49b0a

Browse files
committed
esm: unwrap WebAssembly.Global on Wasm Namespaces
1 parent 36e89dd commit 0f49b0a

File tree

8 files changed

+479
-12
lines changed

8 files changed

+479
-12
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: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypeFilter,
45
ArrayPrototypeMap,
56
ArrayPrototypePush,
67
FunctionPrototypeCall,
78
JSONParse,
8-
ObjectKeys,
9+
ObjectAssign,
910
ObjectPrototypeHasOwnProperty,
1011
ReflectApply,
1112
SafeArrayIterator,
1213
SafeMap,
1314
SafeSet,
15+
SafeWeakMap,
1416
StringPrototypeIncludes,
1517
StringPrototypeReplaceAll,
1618
StringPrototypeSlice,
@@ -483,6 +485,14 @@ translators.set('json', function jsonStrategy(url, source) {
483485
});
484486

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

@@ -501,19 +511,54 @@ translators.set('wasm', async function(url, source) {
501511
throw err;
502512
}
503513

504-
const imports =
505-
ArrayPrototypeMap(WebAssembly.Module.imports(compiled),
506-
({ module }) => module);
507-
const exports =
508-
ArrayPrototypeMap(WebAssembly.Module.exports(compiled),
509-
({ name }) => name);
514+
const wasmImports = WebAssembly.Module.imports(compiled);
515+
const wasmGlobalImports = ArrayPrototypeFilter(wasmImports, ({ kind }) => kind === 'global');
516+
517+
const wasmExports = WebAssembly.Module.exports(compiled);
518+
const wasmGlobalExports = new SafeSet(ArrayPrototypeMap(
519+
ArrayPrototypeFilter(wasmExports, ({ kind }) => kind === 'global'),
520+
({ name }) => name,
521+
));
522+
523+
const importsList = new SafeSet(ArrayPrototypeMap(wasmImports, ({ module }) => module));
524+
const exportsList = ArrayPrototypeMap(wasmExports, ({ name }) => name);
510525

511526
const createDynamicModule = require(
512527
'internal/modules/esm/create_dynamic_module');
513-
const { module } = createDynamicModule(imports, exports, url, (reflect) => {
528+
529+
const { module } = createDynamicModule([...importsList], exportsList, url, (reflect) => {
530+
for (const impt of importsList) {
531+
const importNs = reflect.imports[impt];
532+
const wasmInstance = wasmInstances.get(importNs);
533+
if (wasmInstance) {
534+
const wrappedModule = ObjectAssign({ __proto__: null }, reflect.imports[impt]);
535+
for (const { module, name } of wasmGlobalImports) {
536+
if (module !== impt) {
537+
continue;
538+
}
539+
// Import of Wasm module global -> get direct WebAssembly.Global wrapped value.
540+
// JS API validations otherwise remain the same.
541+
wrappedModule[name] = wasmInstance[name];
542+
}
543+
reflect.imports[impt] = wrappedModule;
544+
}
545+
}
546+
// In cycles importing unexecuted Wasm, wasmInstance will be undefined, which will fail during
547+
// instantiation, since all bindings will be in TDZ.
514548
const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
515-
for (const expt of ObjectKeys(exports)) {
516-
reflect.exports[expt].set(exports[expt]);
549+
wasmInstances.set(module.getNamespace(), exports);
550+
for (const expt of exportsList) {
551+
let val = exports[expt];
552+
// Unwrap WebAssembly.Global for JS bindings
553+
if (wasmGlobalExports.has(expt)) {
554+
// v128 doesn't support ToJsValue() -> undefined (ideally should stay in TDZ)
555+
try {
556+
val = val.value;
557+
} catch {
558+
continue;
559+
}
560+
}
561+
reflect.exports[expt].set(val);
517562
}
518563
});
519564
// WebAssembly modules support source phase imports, to import the compiled module

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

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,142 @@ 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 } from "node:assert";',
72+
`import * as wasmExports from ${JSON.stringify(fixtures.fileURL('es-modules/globals.wasm'))};`,
73+
74+
// Test imported globals using direct access
75+
'strictEqual(wasmExports.importedI32, 42);',
76+
'strictEqual(wasmExports.importedMutI32, 100);',
77+
'strictEqual(wasmExports.importedI64, 9223372036854775807n);',
78+
'strictEqual(wasmExports.importedMutI64, 200n);',
79+
'strictEqual(Math.round(wasmExports.importedF32 * 100000) / 100000, 3.14159);',
80+
'strictEqual(Math.round(wasmExports.importedMutF32 * 100000) / 100000, 2.71828);',
81+
'strictEqual(wasmExports.importedF64, 3.141592653589793);',
82+
'strictEqual(wasmExports.importedMutF64, 2.718281828459045);',
83+
'strictEqual(wasmExports.importedExternref !== null, true);',
84+
'strictEqual(wasmExports.importedMutExternref !== null, true);',
85+
'strictEqual(wasmExports.importedNullExternref, null);',
86+
87+
// Test local globals exported directly
88+
'strictEqual(wasmExports[\'🚀localI32\'], 42);',
89+
'strictEqual(wasmExports.localMutI32, 100);',
90+
'strictEqual(wasmExports.localI64, 9223372036854775807n);',
91+
'strictEqual(wasmExports.localMutI64, 200n);',
92+
'strictEqual(Math.round(wasmExports.localF32 * 100000) / 100000, 3.14159);',
93+
'strictEqual(Math.round(wasmExports.localMutF32 * 100000) / 100000, 2.71828);',
94+
'strictEqual(wasmExports.localF64, 2.718281828459045);',
95+
'strictEqual(wasmExports.localMutF64, 3.141592653589793);',
96+
97+
// Test imported globals using getter functions
98+
'strictEqual(wasmExports.getImportedMutI32(), 100);',
99+
'strictEqual(wasmExports.getImportedMutI64(), 200n);',
100+
'strictEqual(Math.round(wasmExports.getImportedMutF32() * 100000) / 100000, 2.71828);',
101+
'strictEqual(wasmExports.getImportedMutF64(), 2.718281828459045);',
102+
'strictEqual(wasmExports.getImportedMutExternref() !== null, true);',
103+
104+
// Test local globals using getter functions
105+
'strictEqual(wasmExports.getLocalMutI32(), 100);',
106+
'strictEqual(wasmExports.getLocalMutI64(), 200n);',
107+
'strictEqual(Math.round(wasmExports.getLocalMutF32() * 100000) / 100000, 2.71828);',
108+
'strictEqual(wasmExports.getLocalMutF64(), 3.141592653589793);',
109+
'strictEqual(wasmExports.getLocalMutExternref(), null);',
110+
111+
'assert.throws(wasmExports.getLocalMutV128);',
112+
113+
// Pending TDZ support
114+
'strictEqual(wasmExports.depV128, undefined);',
115+
116+
// Test modifying mutable globals and reading the new values
117+
'wasmExports.setImportedMutI32(999);',
118+
'strictEqual(wasmExports.getImportedMutI32(), 999);',
119+
120+
'wasmExports.setImportedMutI64(888n);',
121+
'strictEqual(wasmExports.getImportedMutI64(), 888n);',
122+
123+
'wasmExports.setImportedMutF32(7.77);',
124+
'strictEqual(Math.round(wasmExports.getImportedMutF32() * 100) / 100, 7.77);',
125+
126+
'wasmExports.setImportedMutF64(6.66);',
127+
'strictEqual(wasmExports.getImportedMutF64(), 6.66);',
128+
129+
// Test modifying mutable externref
130+
'const testObj = { test: "object" };',
131+
'wasmExports.setImportedMutExternref(testObj);',
132+
'strictEqual(wasmExports.getImportedMutExternref(), testObj);',
133+
134+
// Test modifying local mutable globals
135+
'wasmExports.setLocalMutI32(555);',
136+
'strictEqual(wasmExports.getLocalMutI32(), 555);',
137+
138+
'wasmExports.setLocalMutI64(444n);',
139+
'strictEqual(wasmExports.getLocalMutI64(), 444n);',
140+
141+
'wasmExports.setLocalMutF32(3.33);',
142+
'strictEqual(Math.round(wasmExports.getLocalMutF32() * 100) / 100, 3.33);',
143+
144+
'wasmExports.setLocalMutF64(2.22);',
145+
'strictEqual(wasmExports.getLocalMutF64(), 2.22);',
146+
147+
// These mutating pending live bindings support
148+
'strictEqual(wasmExports.localMutI32, 100);',
149+
'strictEqual(wasmExports.localMutI64, 200n);',
150+
'strictEqual(Math.round(wasmExports.localMutF32 * 100) / 100, 2.72);',
151+
'strictEqual(wasmExports.localMutF64, 3.141592653589793);',
152+
153+
// Test modifying local mutable externref
154+
'const anotherTestObj = { another: "test object" };',
155+
'wasmExports.setLocalMutExternref(anotherTestObj);',
156+
'strictEqual(wasmExports.getLocalMutExternref(), anotherTestObj);',
157+
'strictEqual(wasmExports.localMutExternref, null);',
158+
159+
// Test dep.wasm imports
160+
'strictEqual(wasmExports.depI32, 1001);',
161+
'strictEqual(wasmExports.depMutI32, 2001);',
162+
'strictEqual(wasmExports.getDepMutI32(), 2001);',
163+
'strictEqual(wasmExports.depI64, 10000000001n);',
164+
'strictEqual(wasmExports.depMutI64, 20000000001n);',
165+
'strictEqual(wasmExports.getDepMutI64(), 20000000001n);',
166+
'strictEqual(Math.round(wasmExports.depF32 * 100) / 100, 10.01);',
167+
'strictEqual(Math.round(wasmExports.depMutF32 * 100) / 100, 20.01);',
168+
'strictEqual(Math.round(wasmExports.getDepMutF32() * 100) / 100, 20.01);',
169+
'strictEqual(wasmExports.depF64, 100.0001);',
170+
'strictEqual(wasmExports.depMutF64, 200.0001);',
171+
'strictEqual(wasmExports.getDepMutF64(), 200.0001);',
172+
173+
// Test modifying dep.wasm mutable globals
174+
'wasmExports.setDepMutI32(3001);',
175+
'strictEqual(wasmExports.getDepMutI32(), 3001);',
176+
177+
'wasmExports.setDepMutI64(30000000001n);',
178+
'strictEqual(wasmExports.getDepMutI64(), 30000000001n);',
179+
180+
'wasmExports.setDepMutF32(30.01);',
181+
'strictEqual(Math.round(wasmExports.getDepMutF32() * 100) / 100, 30.01);',
182+
183+
'wasmExports.setDepMutF64(300.0001);',
184+
'strictEqual(wasmExports.getDepMutF64(), 300.0001);',
185+
186+
// These pending live bindings support
187+
'strictEqual(wasmExports.depMutI32, 2001);',
188+
'strictEqual(wasmExports.depMutI64, 20000000001n);',
189+
'strictEqual(Math.round(wasmExports.depMutF32 * 100) / 100, 20.01);',
190+
'strictEqual(wasmExports.depMutF64, 200.0001);',
56191
].join('\n'),
57192
]);
58193

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)