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