diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 3d6a0fe34d9ef..161dfc0e22f19 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -1856,6 +1856,16 @@ used. An example of using the module is below. foo(); bar(); +The ``init`` function exists so the caller can configure the instance (via +``moduleArg``) before it starts. If there is nothing to configure (i.e. +``INCOMING_MODULE_JS_API`` is empty, as implied by ``STRICT``) the module +instead self-initializes via top-level await and ``init`` is not exported; +the named Wasm/runtime exports are ready to use as soon as it is imported:: + + import { foo, bar } from "./my_module.mjs" + foo(); + bar(); + Default value: false .. _export_es6: diff --git a/src/postamble.js b/src/postamble.js index 39cb9b4c8452e..3a60a3bf29d83 100644 --- a/src/postamble.js +++ b/src/postamble.js @@ -253,12 +253,19 @@ var wasmRawExports; #if ASSERTIONS var initCalled = false; #endif +#if INCOMING_MODULE_JS_API.size == 0 && !WASM_ESM_INTEGRATION +// With no configuration points, `init` is not exported; we self-initialize below. +async function init() { +#else export default async function init(moduleArg = {}) { +#endif #if ASSERTIONS assert(!initCalled); initCalled = true; #endif +#if INCOMING_MODULE_JS_API.size != 0 || WASM_ESM_INTEGRATION Object.assign(Module, moduleArg); +#endif processModuleArgs(); #if WASM_ESM_INTEGRATION #if PTHREADS @@ -276,6 +283,15 @@ export default async function init(moduleArg = {}) { await run(); } +#if INCOMING_MODULE_JS_API.size == 0 && !WASM_ESM_INTEGRATION +#if PTHREADS || WASM_WORKERS +// Worker threads initialize on demand, so only the main thread inits here. +if ({{{ ENVIRONMENT_IS_MAIN_THREAD() }}}) +#endif +await init(); + +#else + #if ENVIRONMENT_MAY_BE_NODE // When run as the main script under node we run `init` immediately. if (ENVIRONMENT_IS_NODE @@ -297,6 +313,8 @@ if (ENVIRONMENT_IS_SHELL) { } #endif +#endif + #else // MODULARIZE == instance #if WASM_WORKERS || PTHREADS diff --git a/src/preamble.js b/src/preamble.js index c95e10b85b09f..2ffdfefbeac66 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -144,7 +144,7 @@ function initRuntime() { #endif #if PTHREADS - if (ENVIRONMENT_IS_PTHREAD) return startWorker(); + if (ENVIRONMENT_IS_PTHREAD) return; #endif #if STACK_OVERFLOW_CHECK >= 2 diff --git a/src/pthread_esm_startup.mjs b/src/pthread_esm_startup.mjs index fb4d2686dcafb..cb6eb9c422820 100644 --- a/src/pthread_esm_startup.mjs +++ b/src/pthread_esm_startup.mjs @@ -48,14 +48,17 @@ self.onmessage = async (msg) => { // Now that we have the wasmMemory we can import the main program globalThis.wasmMemory = msg.data.wasmMemory; + const prog = await import('./{{{ TARGET_JS_NAME }}}'); +#if INCOMING_MODULE_JS_API.size != 0 + await prog.default() +#endif + // Now that the import is completed the main program will have installed // its own `onmessage` handler and replaced our handler. // Now we can dispatch any queued messages to this new handler. for (const msg of messageQueue) { await self.onmessage(msg); } - - await prog.default() }; diff --git a/src/runtime_pthread.js b/src/runtime_pthread.js index d95b514156b5c..db4697f991a4f 100644 --- a/src/runtime_pthread.js +++ b/src/runtime_pthread.js @@ -127,6 +127,9 @@ if (ENVIRONMENT_IS_PTHREAD) { run(); #endif #endif // MINIMAL_RUNTIME +#endif +#if !MINIMAL_RUNTIME + startWorker(); #endif } else if (cmd == {{{ CMD_RUN }}}) { #if ASSERTIONS diff --git a/src/settings.js b/src/settings.js index 3443e70c33669..e402453e0584f 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1281,6 +1281,16 @@ var SMALL_XHR_CHUNKS = false; // foo(); // bar(); // +// The ``init`` function exists so the caller can configure the instance (via +// ``moduleArg``) before it starts. If there is nothing to configure (i.e. +// ``INCOMING_MODULE_JS_API`` is empty, as implied by ``STRICT``) the module +// instead self-initializes via top-level await and ``init`` is not exported; +// the named Wasm/runtime exports are ready to use as soon as it is imported:: +// +// import { foo, bar } from "./my_module.mjs" +// foo(); +// bar(); +// // [link] var MODULARIZE = false; diff --git a/test/codesize/test_codesize_minimal_pthreads.json b/test/codesize/test_codesize_minimal_pthreads.json index 6e7f6f6739a85..6e1d93ed44b56 100644 --- a/test/codesize/test_codesize_minimal_pthreads.json +++ b/test/codesize/test_codesize_minimal_pthreads.json @@ -1,10 +1,10 @@ { - "a.out.js": 6954, - "a.out.js.gz": 3428, + "a.out.js": 6945, + "a.out.js.gz": 3424, "a.out.nodebug.wasm": 19063, "a.out.nodebug.wasm.gz": 8803, - "total": 26017, - "total_gz": 12231, + "total": 26008, + "total_gz": 12227, "sent": [ "a (memory)", "b (exit)", diff --git a/test/codesize/test_codesize_minimal_pthreads_memgrowth.json b/test/codesize/test_codesize_minimal_pthreads_memgrowth.json index d7c9da9b3ec65..9abae0eadf8f9 100644 --- a/test/codesize/test_codesize_minimal_pthreads_memgrowth.json +++ b/test/codesize/test_codesize_minimal_pthreads_memgrowth.json @@ -1,10 +1,10 @@ { - "a.out.js": 7362, - "a.out.js.gz": 3631, + "a.out.js": 7353, + "a.out.js.gz": 3622, "a.out.nodebug.wasm": 19064, "a.out.nodebug.wasm.gz": 8804, - "total": 26426, - "total_gz": 12435, + "total": 26417, + "total_gz": 12426, "sent": [ "a (memory)", "b (exit)", diff --git a/test/test_core.py b/test/test_core.py index 6f8ce66fed726..f78cddbf73bad 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9831,6 +9831,73 @@ def test_modularize_instance(self): self.assertContained('main1\nmain2\nfoo\nbar\nbaz\n', self.run_js('runner.mjs')) + @no_omit_asm_module_exports('MODULARIZE is not compatible with DECLARE_ASM_MODULE_EXPORTS=0') + @no_strict_js('EXPORT_ES6 is not compatible with STRICT_JS') + def test_modularize_instance_tla(self): + create_file('library.js', '''\ + addToLibrary({ + $baz: () => console.log('baz'), + $qux: () => console.log('qux'), + });''') + # An empty INCOMING_MODULE_JS_API means there is nothing to configure, so the + # module self-initializes via top-level await and does not export `init`. + self.run_process([EMCC, test_file('modularize_instance.c'), + '-sMODULARIZE=instance', '-sINCOMING_MODULE_JS_API=[]', + '-Wno-experimental', + '-sEXPORTED_RUNTIME_METHODS=baz,addOnExit,HEAP32', + '-sEXPORTED_FUNCTIONS=_bar,_main,qux', + '--js-library', 'library.js', + '-o', 'modularize_instance.mjs'] + self.get_cflags()) + + # Named exports are usable directly on import; there is no `init` export. + create_file('runner.mjs', ''' + import { strict as assert } from 'assert'; + import * as mod from "./modularize_instance.mjs"; + import { _foo as foo, _bar as bar, baz, qux, addOnExit, HEAP32 } from "./modularize_instance.mjs"; + assert(mod.default === undefined); // no `init` export when self-initializing + foo(); // exported with EMSCRIPTEN_KEEPALIVE + bar(); // exported with EXPORTED_FUNCTIONS + baz(); // exported library function with EXPORTED_RUNTIME_METHODS + qux(); // exported library function with EXPORTED_FUNCTIONS + assert(typeof addOnExit === 'function'); // exported runtime function with EXPORTED_RUNTIME_METHODS + assert(typeof HEAP32 === 'object'); // exported runtime value by default + ''') + + self.assertContained('main1\nmain2\nfoo\nbar\nbaz\n', self.run_js('runner.mjs')) + + @no_omit_asm_module_exports('MODULARIZE is not compatible with DECLARE_ASM_MODULE_EXPORTS=0') + @no_strict_js('EXPORT_ES6 is not compatible with STRICT_JS') + @also_with_pthreads + @esm_integration + def test_esm_integration_tla(self): + self.set_setting('INCOMING_MODULE_JS_API', []) + create_file('library.js', '''\ + addToLibrary({ + $baz: () => console.log('baz'), + $qux: () => console.log('qux'), + });''') + self.run_process([EMCC, test_file('modularize_instance.c'), + '-Wno-experimental', + '-sEXPORTED_RUNTIME_METHODS=baz,addOnExit,HEAP32', + '-sEXPORTED_FUNCTIONS=_bar,_main,qux', + '--js-library', 'library.js', + '-o', 'modularize_instance.mjs'] + self.get_cflags()) + + create_file('runner.mjs', ''' + import { strict as assert } from 'assert'; + import * as mod from "./modularize_instance.mjs"; + import { _foo as foo, _bar as bar, baz, qux, addOnExit, HEAP32 } from "./modularize_instance.mjs"; + assert(mod.default === undefined); // no `init` export when self-initializing + foo(); + bar(); + baz(); + qux(); + assert(typeof addOnExit === 'function'); + assert(typeof HEAP32 === 'object'); + ''') + + self.assertContained('main1\nmain2\nfoo\nbar\nbaz\n', self.run_js('runner.mjs')) + @no_omit_asm_module_exports('MODULARIZE is not compatible with DECLARE_ASM_MODULE_EXPORTS=0') @no_4gb('EMBIND_AOT can\'t lower 4gb') @no_strict_js('EXPORT_ES6 is not compatible with STRICT_JS') diff --git a/tools/link.py b/tools/link.py index 0a01e7544c008..19e091517ca90 100644 --- a/tools/link.py +++ b/tools/link.py @@ -2145,13 +2145,14 @@ def create_esm_wrapper(wrapper_file, support_target, wasm_target): wrapper.append('// in order to avoid issues with circular dependencies.') wrapper.append(f"import * as unused from './{settings.WASM_BINARY_FILE}';") support_url = f'./{os.path.basename(support_target)}' - if js_exports: - wrapper.append(f"export {{ default, {js_exports} }} from '{support_url}';") - else: - wrapper.append(f"export {{ default }} from '{support_url}';") + if settings.INCOMING_MODULE_JS_API: + if js_exports: + wrapper.append(f"export {{ default, {js_exports} }} from '{support_url}';") + else: + wrapper.append(f"export {{ default }} from '{support_url}';") - if settings.ENVIRONMENT_MAY_BE_NODE: - wrapper.append(f''' + if settings.ENVIRONMENT_MAY_BE_NODE: + wrapper.append(f''' // When run as the main module under node, create the module directly. This will // execute any startup code along with main (if it exists). import init from '{support_url}'; @@ -2161,6 +2162,13 @@ def create_esm_wrapper(wrapper_file, support_target, wasm_target): const isMainModule = url.pathToFileURL(process.argv[1]).href === import.meta.url; if (isMainModule) await init(); }}''') + else: + # With no configuration points, self-initialize via top-level await and + # don't re-export `init`. + if js_exports: + wrapper.append(f"export {{ {js_exports} }} from '{support_url}';") + wrapper.append(f"import init from '{support_url}';") + wrapper.append('await init();') write_file(wrapper_file, '\n'.join(wrapper) + '\n')