Skip to content
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

esm: WebAssembly.Global unwrapping for Wasm Namespaces #57525

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/create_dynamic_module.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import.meta.exports[${nameStringLit}] = {
* @param {string} [url=''] - The URL of the module.
* @param {(reflect: DynamicModuleReflect) => void} evaluate - The function to evaluate the module.
* @typedef {object} DynamicModuleReflect
* @property {string[]} imports - The imports of the module.
* @property {Record<string, Record<string, any>>} imports - The imports of the module.
* @property {string[]} exports - The exports of the module.
* @property {(cb: (reflect: DynamicModuleReflect) => void) => void} onReady - Callback to evaluate the module.
*/
Expand Down
65 changes: 55 additions & 10 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
'use strict';

const {
ArrayPrototypeFilter,
ArrayPrototypeMap,
ArrayPrototypePush,
FunctionPrototypeCall,
JSONParse,
ObjectKeys,
ObjectAssign,
ObjectPrototypeHasOwnProperty,
ReflectApply,
SafeArrayIterator,
SafeMap,
SafeSet,
SafeWeakMap,
StringPrototypeIncludes,
StringPrototypeReplaceAll,
StringPrototypeSlice,
Expand Down Expand Up @@ -483,6 +485,14 @@ translators.set('json', function jsonStrategy(url, source) {
});

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

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

const imports =
ArrayPrototypeMap(WebAssembly.Module.imports(compiled),
({ module }) => module);
const exports =
ArrayPrototypeMap(WebAssembly.Module.exports(compiled),
({ name }) => name);
const wasmImports = WebAssembly.Module.imports(compiled);
const wasmGlobalImports = ArrayPrototypeFilter(wasmImports, ({ kind }) => kind === 'global');

const wasmExports = WebAssembly.Module.exports(compiled);
const wasmGlobalExports = new SafeSet(ArrayPrototypeMap(
ArrayPrototypeFilter(wasmExports, ({ kind }) => kind === 'global'),
({ name }) => name,
));

const importsList = new SafeSet(ArrayPrototypeMap(wasmImports, ({ module }) => module));
const exportsList = ArrayPrototypeMap(wasmExports, ({ name }) => name);

const createDynamicModule = require(
'internal/modules/esm/create_dynamic_module');
const { module } = createDynamicModule(imports, exports, url, (reflect) => {

const { module } = createDynamicModule([...importsList], exportsList, url, (reflect) => {
for (const impt of importsList) {
const importNs = reflect.imports[impt];
const wasmInstance = wasmInstances.get(importNs);
if (wasmInstance) {
const wrappedModule = ObjectAssign({ __proto__: null }, reflect.imports[impt]);
for (const { module, name } of wasmGlobalImports) {
if (module !== impt) {
continue;
}
// Import of Wasm module global -> get direct WebAssembly.Global wrapped value.
// JS API validations otherwise remain the same.
wrappedModule[name] = wasmInstance[name];
}
reflect.imports[impt] = wrappedModule;
}
}
// In cycles importing unexecuted Wasm, wasmInstance will be undefined, which will fail during
// instantiation, since all bindings will be in TDZ.
const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
for (const expt of ObjectKeys(exports)) {
reflect.exports[expt].set(exports[expt]);
wasmInstances.set(module.getNamespace(), exports);
for (const expt of exportsList) {
let val = exports[expt];
// Unwrap WebAssembly.Global for JS bindings
if (wasmGlobalExports.has(expt)) {
// v128 doesn't support ToJsValue() -> undefined (ideally should stay in TDZ)
try {
val = val.value;
} catch {
continue;
}
}
reflect.exports[expt].set(val);
}
});
// WebAssembly modules support source phase imports, to import the compiled module
Expand Down
137 changes: 136 additions & 1 deletion test/es-module/test-esm-wasm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,142 @@ describe('ESM: WASM modules', { concurrency: !process.env.TEST_PARALLEL }, () =>
[
'import { strictEqual } from "node:assert";',
`import * as wasmExports from ${JSON.stringify(fixtures.fileURL('es-modules/export-name-syntax-error.wasm'))};`,
'assert.strictEqual(wasmExports["?f!o:o<b>a[r]"]?.value, 12682);',
'assert.strictEqual(wasmExports["?f!o:o<b>a[r]"], 12682);',
].join('\n'),
]);

strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
});

it('should properly handle all WebAssembly global types', async () => {
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-wasm-modules',
'--input-type=module',
'--eval',
[
'import { strictEqual, deepStrictEqual } from "node:assert";',
`import * as wasmExports from ${JSON.stringify(fixtures.fileURL('es-modules/globals.wasm'))};`,

// Test imported globals using direct access
'strictEqual(wasmExports.importedI32, 42);',
'strictEqual(wasmExports.importedMutI32, 100);',
'strictEqual(wasmExports.importedI64, 9223372036854775807n);',
'strictEqual(wasmExports.importedMutI64, 200n);',
'strictEqual(Math.round(wasmExports.importedF32 * 100000) / 100000, 3.14159);',
'strictEqual(Math.round(wasmExports.importedMutF32 * 100000) / 100000, 2.71828);',
'strictEqual(wasmExports.importedF64, 3.141592653589793);',
'strictEqual(wasmExports.importedMutF64, 2.718281828459045);',
'strictEqual(wasmExports.importedExternref !== null, true);',
'strictEqual(wasmExports.importedMutExternref !== null, true);',
'strictEqual(wasmExports.importedNullExternref, null);',

// Test local globals exported directly
'strictEqual(wasmExports[\'🚀localI32\'], 42);',
'strictEqual(wasmExports.localMutI32, 100);',
'strictEqual(wasmExports.localI64, 9223372036854775807n);',
'strictEqual(wasmExports.localMutI64, 200n);',
'strictEqual(Math.round(wasmExports.localF32 * 100000) / 100000, 3.14159);',
'strictEqual(Math.round(wasmExports.localMutF32 * 100000) / 100000, 2.71828);',
'strictEqual(wasmExports.localF64, 2.718281828459045);',
'strictEqual(wasmExports.localMutF64, 3.141592653589793);',

// Test imported globals using getter functions
'strictEqual(wasmExports.getImportedMutI32(), 100);',
'strictEqual(wasmExports.getImportedMutI64(), 200n);',
'strictEqual(Math.round(wasmExports.getImportedMutF32() * 100000) / 100000, 2.71828);',
'strictEqual(wasmExports.getImportedMutF64(), 2.718281828459045);',
'strictEqual(wasmExports.getImportedMutExternref() !== null, true);',

// Test local globals using getter functions
'strictEqual(wasmExports.getLocalMutI32(), 100);',
'strictEqual(wasmExports.getLocalMutI64(), 200n);',
'strictEqual(Math.round(wasmExports.getLocalMutF32() * 100000) / 100000, 2.71828);',
'strictEqual(wasmExports.getLocalMutF64(), 3.141592653589793);',
'strictEqual(wasmExports.getLocalMutExternref(), null);',

'assert.throws(wasmExports.getLocalMutV128);',

// Pending TDZ support
'strictEqual(wasmExports.depV128, undefined);',

// Test modifying mutable globals and reading the new values
'wasmExports.setImportedMutI32(999);',
'strictEqual(wasmExports.getImportedMutI32(), 999);',

'wasmExports.setImportedMutI64(888n);',
'strictEqual(wasmExports.getImportedMutI64(), 888n);',

'wasmExports.setImportedMutF32(7.77);',
'strictEqual(Math.round(wasmExports.getImportedMutF32() * 100) / 100, 7.77);',

'wasmExports.setImportedMutF64(6.66);',
'strictEqual(wasmExports.getImportedMutF64(), 6.66);',

// Test modifying mutable externref
'const testObj = { test: "object" };',
'wasmExports.setImportedMutExternref(testObj);',
'strictEqual(wasmExports.getImportedMutExternref(), testObj);',

// Test modifying local mutable globals
'wasmExports.setLocalMutI32(555);',
'strictEqual(wasmExports.getLocalMutI32(), 555);',

'wasmExports.setLocalMutI64(444n);',
'strictEqual(wasmExports.getLocalMutI64(), 444n);',

'wasmExports.setLocalMutF32(3.33);',
'strictEqual(Math.round(wasmExports.getLocalMutF32() * 100) / 100, 3.33);',

'wasmExports.setLocalMutF64(2.22);',
'strictEqual(wasmExports.getLocalMutF64(), 2.22);',

// These mutating pending live bindings support
'strictEqual(wasmExports.localMutI32, 100);',
'strictEqual(wasmExports.localMutI64, 200n);',
'strictEqual(Math.round(wasmExports.localMutF32 * 100) / 100, 2.72);',
'strictEqual(wasmExports.localMutF64, 3.141592653589793);',

// Test modifying local mutable externref
'const anotherTestObj = { another: "test object" };',
'wasmExports.setLocalMutExternref(anotherTestObj);',
'strictEqual(wasmExports.getLocalMutExternref(), anotherTestObj);',
'strictEqual(wasmExports.localMutExternref, null);',

// Test dep.wasm imports
'strictEqual(wasmExports.depI32, 1001);',
'strictEqual(wasmExports.depMutI32, 2001);',
'strictEqual(wasmExports.getDepMutI32(), 2001);',
'strictEqual(wasmExports.depI64, 10000000001n);',
'strictEqual(wasmExports.depMutI64, 20000000001n);',
'strictEqual(wasmExports.getDepMutI64(), 20000000001n);',
'strictEqual(Math.round(wasmExports.depF32 * 100) / 100, 10.01);',
'strictEqual(Math.round(wasmExports.depMutF32 * 100) / 100, 20.01);',
'strictEqual(Math.round(wasmExports.getDepMutF32() * 100) / 100, 20.01);',
'strictEqual(wasmExports.depF64, 100.0001);',
'strictEqual(wasmExports.depMutF64, 200.0001);',
'strictEqual(wasmExports.getDepMutF64(), 200.0001);',

// Test modifying dep.wasm mutable globals
'wasmExports.setDepMutI32(3001);',
'strictEqual(wasmExports.getDepMutI32(), 3001);',

'wasmExports.setDepMutI64(30000000001n);',
'strictEqual(wasmExports.getDepMutI64(), 30000000001n);',

'wasmExports.setDepMutF32(30.01);',
'strictEqual(Math.round(wasmExports.getDepMutF32() * 100) / 100, 30.01);',

'wasmExports.setDepMutF64(300.0001);',
'strictEqual(wasmExports.getDepMutF64(), 300.0001);',

// These pending live bindings support
'strictEqual(wasmExports.depMutI32, 2001);',
'strictEqual(wasmExports.depMutI64, 20000000001n);',
'strictEqual(Math.round(wasmExports.depMutF32 * 100) / 100, 20.01);',
'strictEqual(wasmExports.depMutF64, 200.0001);',
].join('\n'),
]);

Expand Down
Binary file added test/fixtures/es-modules/dep.wasm
Binary file not shown.
32 changes: 32 additions & 0 deletions test/fixtures/es-modules/dep.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
(module
(type (;0;) (func (result i32)))
(type (;1;) (func (result i64)))
(type (;2;) (func (result f32)))
(type (;3;) (func (result f64)))
(type (;4;) (func (result externref)))
(type (;5;) (func (result v128)))
(global $i32_value (;0;) i32 i32.const 1001)
(global $i32_mut_value (;1;) (mut i32) i32.const 2001)
(global $i64_value (;2;) i64 i64.const 10000000001)
(global $i64_mut_value (;3;) (mut i64) i64.const 20000000001)
(global $f32_value (;4;) f32 f32.const 0x1.4051ecp+3 (;=10.01;))
(global $f32_mut_value (;5;) (mut f32) f32.const 0x1.4028f6p+4 (;=20.01;))
(global $f64_value (;6;) f64 f64.const 0x1.90001a36e2eb2p+6 (;=100.0001;))
(global $f64_mut_value (;7;) (mut f64) f64.const 0x1.90000d1b71759p+7 (;=200.0001;))
(global $externref_value (;8;) externref ref.null extern)
(global $externref_mut_value (;9;) (mut externref) ref.null extern)
(global $v128_value (;10;) v128 v128.const i32x4 0x0000000a 0x00000014 0x0000001e 0x00000028)
(global $v128_mut_value (;11;) (mut v128) v128.const i32x4 0x00000032 0x0000003c 0x00000046 0x00000050)
(export "i32_value" (global $i32_value))
(export "i32_mut_value" (global $i32_mut_value))
(export "i64_value" (global $i64_value))
(export "i64_mut_value" (global $i64_mut_value))
(export "f32_value" (global $f32_value))
(export "f32_mut_value" (global $f32_mut_value))
(export "f64_value" (global $f64_value))
(export "f64_mut_value" (global $f64_mut_value))
(export "externref_value" (global $externref_value))
(export "externref_mut_value" (global $externref_mut_value))
(export "v128_value" (global $v128_value))
(export "v128_mut_value" (global $v128_mut_value))
)
18 changes: 18 additions & 0 deletions test/fixtures/es-modules/globals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// globals.js - Direct global exports for WebAssembly imports

// Immutable globals (simple values)
const i32_value = 42;
export { i32_value as '🚀i32_value' }
export const i64_value = 9223372036854775807n; // Max i64 value
export const f32_value = 3.14159;
export const f64_value = 3.141592653589793;

// Mutable globals with WebAssembly.Global wrapper
export const i32_mut_value = new WebAssembly.Global({ value: 'i32', mutable: true }, 100);
export const i64_mut_value = new WebAssembly.Global({ value: 'i64', mutable: true }, 200n);
export const f32_mut_value = new WebAssembly.Global({ value: 'f32', mutable: true }, 2.71828);
export const f64_mut_value = new WebAssembly.Global({ value: 'f64', mutable: true }, 2.718281828459045);

export const externref_value = { hello: 'world' };
export const externref_mut_value = new WebAssembly.Global({ value: 'externref', mutable: true }, { mutable: 'global' });
export const null_externref_value = null;
Binary file added test/fixtures/es-modules/globals.wasm
Binary file not shown.
Loading