Skip to content
Merged
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
5 changes: 5 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ See docs/process.md for more on how version tagging works.
supported, with 256-bit variants emulated via two 128-bit operations. Pass
``-msimd128 -mfma`` to enable. With ``-mrelaxed-simd -mfma``, Wasm relaxed
SIMD FMA is used. (#27183)
- New `AUTO_INIT` setting to opt an instance ES module (`MODULARIZE=instance` or
`WASM_ESM_INTEGRATION`) into self-initialization via top-level await on import,
rather than exporting a default `init` function. Since there is no
init/moduleArg, module-level configuration is unavailable:
`INCOMING_MODULE_JS_API` is disabled and passing a non-empty one is an error.
- The async `poll()`/`select()` implementation was refactored onto a per-inode
readiness wait-queue. As part of this, the (undocumented) `stream_ops.poll`
FS-backend handler signature changed from `poll(stream, timeout)` to
Expand Down
31 changes: 31 additions & 0 deletions site/source/docs/tools_reference/settings_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,37 @@ 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. When there is nothing to configure, see
``AUTO_INIT`` to have the module self-initialize on import.

Default value: false

.. _auto_init:

AUTO_INIT
=========

When set, an instance ES module (``MODULARIZE=instance`` or
``WASM_ESM_INTEGRATION``) initializes itself via top-level await on import
rather than exporting an ``init`` function to be called by the consumer. The
named Wasm/runtime exports are ready to use as soon as the module is
imported::

import { foo, bar } from "./my_module.mjs"
foo();
bar();

Since the module initializes without any caller involvement, there is no
opportunity for module-level configuration: ``moduleArg`` cannot be passed
and the entire ``INCOMING_MODULE_JS_API`` is disabled (passing a non-empty
``INCOMING_MODULE_JS_API`` is an error).

Because no default ``init`` export is emitted, this also frees up the
``default`` export name for the program's own use.

Requires ``MODULARIZE=instance`` or ``WASM_ESM_INTEGRATION``.

Default value: false

.. _export_es6:
Expand Down
19 changes: 19 additions & 0 deletions src/postamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,19 @@ var wasmRawExports;
#if ASSERTIONS
var initCalled = false;
#endif
#if AUTO_INIT && !WASM_ESM_INTEGRATION
// In AUTO_INIT mode `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 !AUTO_INIT || WASM_ESM_INTEGRATION
Object.assign(Module, moduleArg);
#endif
processModuleArgs();
#if WASM_ESM_INTEGRATION
#if PTHREADS
Expand All @@ -272,6 +279,16 @@ export default async function init(moduleArg = {}) {
await run();
}

#if AUTO_INIT && !WASM_ESM_INTEGRATION
#if PTHREADS || WASM_WORKERS
// Worker threads self-init on demand from the CMD_LOAD handler (see
// runtime_pthread.js), so only the main thread inits here.
if ({{{ ENVIRONMENT_IS_MAIN_THREAD() }}})
#endif
await init();
Comment thread
guybedford marked this conversation as resolved.

#else

#if ENVIRONMENT_MAY_BE_NODE
// When run as the main script under node we run `init` immediately.
if (ENVIRONMENT_IS_NODE
Expand All @@ -293,6 +310,8 @@ if (ENVIRONMENT_IS_SHELL) {
}
#endif

#endif

#else // MODULARIZE == instance

#if WASM_WORKERS || PTHREADS
Expand Down
2 changes: 1 addition & 1 deletion src/preamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ function initRuntime() {
#endif

#if PTHREADS
if (ENVIRONMENT_IS_PTHREAD) return startWorker();
if (ENVIRONMENT_IS_PTHREAD) return;
Comment thread
guybedford marked this conversation as resolved.
#endif

#if STACK_OVERFLOW_CHECK >= 2
Expand Down
7 changes: 5 additions & 2 deletions src/pthread_esm_startup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 !AUTO_INIT
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()
};
3 changes: 3 additions & 0 deletions src/runtime_pthread.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1332,9 +1332,35 @@ var SMALL_XHR_CHUNKS = false;
// foo();
// bar();
//
// The ``init`` function exists so the caller can configure the instance (via
// ``moduleArg``) before it starts. When there is nothing to configure, see
// ``AUTO_INIT`` to have the module self-initialize on import.
//
// [link]
var MODULARIZE = false;

// When set, an instance ES module (``MODULARIZE=instance`` or
// ``WASM_ESM_INTEGRATION``) initializes itself via top-level await on import
// rather than exporting an ``init`` function to be called by the consumer. The
// named Wasm/runtime exports are ready to use as soon as the module is
// imported::
//
// import { foo, bar } from "./my_module.mjs"
// foo();
// bar();
//
// Since the module initializes without any caller involvement, there is no
// opportunity for module-level configuration: ``moduleArg`` cannot be passed
// and the entire ``INCOMING_MODULE_JS_API`` is disabled (passing a non-empty
// ``INCOMING_MODULE_JS_API`` is an error).
//
// Because no default ``init`` export is emitted, this also frees up the
// ``default`` export name for the program's own use.
//
// Requires ``MODULARIZE=instance`` or ``WASM_ESM_INTEGRATION``.
// [link]
var AUTO_INIT = false;

// Export using an ES6 Module export rather than a UMD export. MODULARIZE must
// be enabled for ES6 exports and is implicitly enabled if not already set.
//
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_minimal_pthreads.json
Original file line number Diff line number Diff line change
@@ -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)",
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_minimal_pthreads_memgrowth.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"a.out.js": 7552,
"a.out.js.gz": 3708,
"a.out.js": 7543,
"a.out.js.gz": 3705,
"a.out.nodebug.wasm": 19064,
"a.out.nodebug.wasm.gz": 8804,
"total": 26616,
"total_gz": 12512,
"total": 26607,
"total_gz": 12509,
"sent": [
"a (memory)",
"b (exit)",
Expand Down
65 changes: 65 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -6216,6 +6216,71 @@ def test_modularize_instance_run_dependency(self):
expected = 'add-dep\nremove-dep\nHello, world!\ngot module\n'
self.assertContained(expected, self.run_js('run.mjs'))

def test_modularize_instance_auto_init(self):
create_file('library.js', '''\
addToLibrary({
$baz: () => console.log('baz'),
$qux: () => console.log('qux'),
});''')
# With AUTO_INIT the module self-initializes via top-level await and does not
# export `init`.
self.run_process([EMCC, test_file('modularize_instance.c'),
'-sMODULARIZE=instance', '-sAUTO_INIT',
'-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'))

@also_with_pthreads
@requires_node_25
def test_esm_integration_auto_init(self):
# Instance-phase wasm imports currently generate an ExperimentalWarning under node.
self.node_args += ['--no-warnings']
Comment thread
guybedford marked this conversation as resolved.
create_file('library.js', '''\
addToLibrary({
$baz: () => console.log('baz'),
$qux: () => console.log('qux'),
});''')
self.run_process([EMCC, test_file('modularize_instance.c'),
'-Wno-experimental',
'-sWASM_ESM_INTEGRATION', '-sAUTO_INIT',
'-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'))

def test_modularize_instantiation_error(self):
self.run_process([EMCC, test_file('hello_world.c'), '-o', 'out.mjs'] + self.get_cflags())
create_file('run.mjs', '''
Expand Down
27 changes: 22 additions & 5 deletions tools/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,15 @@ def limit_incoming_module_api():
if options.use_preload_plugins or options.preload_files:
exit_with_error('MODULARIZE=instance is not compatible with --embed-file/--preload-file')

if settings.AUTO_INIT:
if settings.MODULARIZE != 'instance':
exit_with_error('AUTO_INIT requires MODULARIZE=instance or WASM_ESM_INTEGRATION')
# There is no `init`/`moduleArg` to configure the instance with, so any
# incoming module API is meaningless.
if 'INCOMING_MODULE_JS_API' in user_settings:
exit_with_error('AUTO_INIT is not compatible with INCOMING_MODULE_JS_API')
settings.INCOMING_MODULE_JS_API = []

if settings.MINIMAL_RUNTIME and options.preload_files:
exit_with_error('MINIMAL_RUNTIME is not compatible with --preload-file')

Expand Down Expand Up @@ -2158,13 +2167,21 @@ 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}';")
if settings.AUTO_INIT:
# Self-initialize via top-level await and don't re-export `init`, freeing up
# the `default` export name for the program's own use.
if js_exports:
wrapper.append(f"export {{ {js_exports} }} from '{support_url}';")
wrapper.append(f"import init from '{support_url}';")
wrapper.append('await init();')
else:
wrapper.append(f"export {{ default }} from '{support_url}';")
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}';
Expand Down