From 836456519e9fe18982540a8c7a148b6b62da9e10 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Mon, 2 Dec 2024 20:56:25 -0800 Subject: [PATCH] Add option to use source phase imports for wasm module loading Now that node support has been landed we can test this, at least against the latest node canary builds. See https://github.com/nodejs/node/pull/56919 Fixes: #23047 --- ChangeLog.md | 3 ++ .../tools_reference/settings_reference.rst | 12 +++++ src/preamble.js | 48 +++++++++++-------- src/settings.js | 7 +++ test/test_other.py | 14 +++++- tools/link.py | 6 +++ 6 files changed, 68 insertions(+), 22 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 00ca174bb5e1c..98f18fdc205cc 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,9 @@ See docs/process.md for more on how version tagging works. 4.0.5 (in development) ---------------------- +- Added initial support for wasm source phase imports via + `-sSOURCE_PHASE_IMPORTS`. This is currently experimental and not yet + implemented in browsers. (#23175) 4.0.4 - 02/25/25 ---------------- diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index b0a81110c0fc5..457de8dbac77e 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3347,3 +3347,15 @@ Use _ for non-pointer arguments, p for pointer/i53 arguments, and P for optional Example use -sSIGNATURE_CONVERSIONS=someFunction:_p,anotherFunction:p Default value: [] + +.. _source_phase_imports: + +SOURCE_PHASE_IMPORTS +==================== + +Experimental support for wasm source phase imports. +This is only currently implemented in the pre-release/nightly version of node, +and not yet supported by browsers. +Requires EXPORT_ES6 + +Default value: false diff --git a/src/preamble.js b/src/preamble.js index 4d516dab9d9a1..9c8bb81d044c2 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -558,11 +558,32 @@ function instrumentWasmTableWithAbort() { } #endif +#if LOAD_SOURCE_MAP +function receiveSourceMapJSON(sourceMap) { + wasmSourceMap = new WasmSourceMap(sourceMap); + {{{ runIfMainThread("removeRunDependency('source-map');") }}} +} +#endif + +#if (PTHREADS || WASM_WORKERS) && (LOAD_SOURCE_MAP || USE_OFFSET_CONVERTER) +// When using postMessage to send an object, it is processed by the structured +// clone algorithm. The prototype, and hence methods, on that object is then +// lost. This function adds back the lost prototype. This does not work with +// nested objects that has prototypes, but it suffices for WasmSourceMap and +// WasmOffsetConverter. +function resetPrototype(constructor, attrs) { + var object = Object.create(constructor.prototype); + return Object.assign(object, attrs); +} +#endif + +#if !SOURCE_PHASE_IMPORTS #if SINGLE_FILE // In SINGLE_FILE mode the wasm binary is encoded inline here as a data: URL. var wasmBinaryFile = '{{{ WASM_BINARY_FILE }}}'; #else var wasmBinaryFile; + function findWasmBinary() { #if EXPORT_ES6 && !AUDIO_WORKLET if (Module['locateFile']) { @@ -647,13 +668,6 @@ var splitModuleProxyHandler = { }; #endif -#if LOAD_SOURCE_MAP -function receiveSourceMapJSON(sourceMap) { - wasmSourceMap = new WasmSourceMap(sourceMap); - {{{ runIfMainThread("removeRunDependency('source-map');") }}} -} -#endif - #if SPLIT_MODULE || !WASM_ASYNC_COMPILATION function instantiateSync(file, info) { var module; @@ -701,18 +715,6 @@ function instantiateSync(file, info) { } #endif -#if (PTHREADS || WASM_WORKERS) && (LOAD_SOURCE_MAP || USE_OFFSET_CONVERTER) -// When using postMessage to send an object, it is processed by the structured -// clone algorithm. The prototype, and hence methods, on that object is then -// lost. This function adds back the lost prototype. This does not work with -// nested objects that has prototypes, but it suffices for WasmSourceMap and -// WasmOffsetConverter. -function resetPrototype(constructor, attrs) { - var object = Object.create(constructor.prototype); - return Object.assign(object, attrs); -} -#endif - #if WASM_ASYNC_COMPILATION async function instantiateArrayBuffer(binaryFile, imports) { try { @@ -815,6 +817,7 @@ async function instantiateAsync(binary, binaryFile, imports) { return instantiateArrayBuffer(binaryFile, imports); } #endif // WASM_ASYNC_COMPILATION +#endif // SOURCE_PHASE_IMPORTS function getWasmImports() { #if PTHREADS @@ -1016,10 +1019,14 @@ function getWasmImports() { } #endif +#if SOURCE_PHASE_IMPORTS + var instance = await WebAssembly.instantiate(wasmModule, info); + var exports = receiveInstantiationResult({instance, 'module':wasmModule}); + return exports; +#else #if !SINGLE_FILE wasmBinaryFile ??= findWasmBinary(); #endif - #if WASM_ASYNC_COMPILATION #if RUNTIME_DEBUG dbg('asynchronously preparing wasm'); @@ -1051,6 +1058,7 @@ function getWasmImports() { return receiveInstance(result[0]); #endif #endif // WASM_ASYNC_COMPILATION +#endif // SOURCE_PHASE_IMPORTS } #if !WASM_BIGINT diff --git a/src/settings.js b/src/settings.js index 533a797aad347..fd222923a1648 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2182,6 +2182,13 @@ var LEGACY_RUNTIME = false; // [link] var SIGNATURE_CONVERSIONS = []; +// Experimental support for wasm source phase imports. +// This is only currently implemented in the pre-release/nightly version of node, +// and not yet supported by browsers. +// Requires EXPORT_ES6 +// [link] +var SOURCE_PHASE_IMPORTS = false; + // For renamed settings the format is: // [OLD_NAME, NEW_NAME] // For removed settings (which now effectively have a fixed value and can no diff --git a/test/test_other.py b/test/test_other.py index 77abf6706d41a..e9355fbd0ce0d 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -358,13 +358,23 @@ def test_emcc_generate_config(self, compiler): @parameterized({ '': ([],), 'node': (['-sENVIRONMENT=node'],), + # load a worker before startup to check ES6 modules there as well + 'pthreads': (['-pthread', '-sPTHREAD_POOL_SIZE=1'],), }) def test_esm(self, args): self.run_process([EMCC, '-o', 'hello_world.mjs', '--extern-post-js', test_file('modularize_post_js.js'), test_file('hello_world.c')] + args) - src = read_file('hello_world.mjs') - self.assertContained('export default Module;', src) + self.assertContained('export default Module;', read_file('hello_world.mjs')) + self.assertContained('hello, world!', self.run_js('hello_world.mjs')) + + @requires_node_canary + def test_esm_source_phase_imports(self): + self.node_args += ['--experimental-wasm-modules'] + self.run_process([EMCC, '-o', 'hello_world.mjs', '-sSOURCE_PHASE_IMPORTS', + '--extern-post-js', test_file('modularize_post_js.js'), + test_file('hello_world.c')]) + self.assertContained('import source wasmModule from', read_file('hello_world.mjs')) self.assertContained('hello, world!', self.run_js('hello_world.mjs')) @parameterized({ diff --git a/tools/link.py b/tools/link.py index a612a338a55e1..9440e8908a858 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1757,6 +1757,9 @@ def get_full_import_name(name): if settings.ASYNCIFY == 2: diagnostics.warning('experimental', '-sASYNCIFY=2 (JSPI) is still experimental') + if settings.SOURCE_PHASE_IMPORTS: + diagnostics.warning('experimental', '-sSOURCE_PHASE_IMPORTS is still experimental and not yet supported in browsers') + if settings.WASM2JS: if settings.GENERATE_SOURCE_MAP: exit_with_error('wasm2js does not support source maps yet (debug in wasm for now)') @@ -2478,6 +2481,9 @@ def modularize(): })(); ''' % {'EXPORT_NAME': settings.EXPORT_NAME} + if settings.SOURCE_PHASE_IMPORTS: + src = f"import source wasmModule from './{settings.WASM_BINARY_FILE}';\n\n" + src + # Given the async nature of how the Module function and Module object # come into existence in AudioWorkletGlobalScope, store the Module # function under a different variable name so that AudioWorkletGlobalScope