Skip to content

Commit 968bbbc

Browse files
committed
[esm-integration] Add pthread support
Under ESM integration all dependencies must be satisfied at import time, but pthreads requires that we have supply the memory via postMessage, so the memory is, by definition, not available at import time. On order to work around this issue we create a smaller pthread stub/loader file that delays the import of the main program until the initial `postMessage` has been received. Once the memory is received we load main program using a dynamic `import` statement.
1 parent 97cba27 commit 968bbbc

14 files changed

+104
-14
lines changed

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default [{
3535
'src/postamble*.js',
3636
'src/closure-externs/',
3737
'src/embind/',
38+
'src/pthread_esm_startup.mjs',
3839
'src/emrun_postjs.js',
3940
'src/wasm_worker.js',
4041
'src/audio_worklet.js',

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
},
2727
"scripts": {
2828
"lint": "eslint .",
29-
"fmt": "prettier --write src/*.mjs tools/*.mjs",
30-
"check": "prettier --check src/*.mjs tools/*.mjs"
29+
"fmt": "prettier --write src/*.mjs tools/*.mjs --ignore-path src/pthread_esm_startup.mjs",
30+
"check": "prettier --check src/*.mjs tools/*.mjs --ignore-path src/pthread_esm_startup.mjs"
3131
}
3232
}

site/source/docs/compiling/Modularized-Output.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ This setting implicitly enables :ref:`export_es6` and sets :ref:`MODULARIZE` to
163163

164164
Some additional limitations are:
165165

166-
* ``-pthread`` / :ref:`wasm_workers` are not yet supported.
166+
* :ref:`wasm_workers` is not yet supported.
167167

168168
* :ref:`abort_on_wasm_exceptions` is not supported (requires wrapping wasm
169169
exports).

src/lib/libpthread.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ const MAX_PTR = Number((2n ** 64n) - 1n);
2929
#else
3030
const MAX_PTR = (2 ** 32) - 1
3131
#endif
32+
33+
#if WASM_ESM_INTEGRATION
34+
const pthreadWorkerScript = TARGET_BASENAME + '.pthread.mjs';
35+
#else
36+
const pthreadWorkerScript = TARGET_JS_NAME;
37+
#endif
38+
3239
// Use a macro to avoid duplicating pthread worker options.
3340
// We cannot use a normal JS variable since the vite bundler requires that worker
3441
// options be inline.
@@ -295,7 +302,9 @@ var LibraryPThread = {
295302

296303
#if ASSERTIONS
297304
assert(wasmMemory instanceof WebAssembly.Memory, 'WebAssembly memory should have been loaded by now!');
305+
#if !WASM_ESM_INTEGRATION
298306
assert(wasmModule instanceof WebAssembly.Module, 'WebAssembly Module should have been loaded by now!');
307+
#endif
299308
#endif
300309

301310
// When running on a pthread, none of the incoming parameters on the module
@@ -333,7 +342,9 @@ var LibraryPThread = {
333342
#else // WASM2JS
334343
wasmMemory,
335344
#endif // WASM2JS
345+
#if !WASM_ESM_INTEGRATION
336346
wasmModule,
347+
#endif
337348
#if LOAD_SOURCE_MAP
338349
wasmSourceMap,
339350
#endif
@@ -391,7 +402,7 @@ var LibraryPThread = {
391402
#if TRUSTED_TYPES
392403
// Use Trusted Types compatible wrappers.
393404
if (typeof trustedTypes != 'undefined' && trustedTypes.createPolicy) {
394-
var p = trustedTypes.createPolicy('emscripten#workerPolicy1', { createScriptURL: (ignored) => import.meta.url });
405+
var p = trustedTypes.createPolicy('emscripten#workerPolicy1', { createScriptURL: (ignored) => new URL('{{{ pthreadWorkerScript }}}', import.meta.url) });
395406
worker = new Worker(p.createScriptURL('ignored'), {{{ pthreadWorkerOptions }}});
396407
} else
397408
#endif
@@ -409,7 +420,7 @@ var LibraryPThread = {
409420
// the first case in their bundling step. The latter ends up producing an invalid
410421
// URL to import from the server (e.g., for webpack the file:// path).
411422
// See https://github.com/webpack/webpack/issues/12638
412-
worker = new Worker(new URL('{{{ TARGET_JS_NAME }}}', import.meta.url), {{{ pthreadWorkerOptions }}});
423+
worker = new Worker(new URL('{{{ pthreadWorkerScript }}}', import.meta.url), {{{ pthreadWorkerOptions }}});
413424
#else // EXPORT_ES6
414425
var pthreadMainJs = _scriptName;
415426
#if expectToReceiveOnModule('mainScriptUrlOrBlob')

src/modularize.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// JS program code (INNER_JS_CODE) and wrapping it in a factory function.
99

1010
#if SOURCE_PHASE_IMPORTS
11-
import source wasmModule from './{settings.WASM_BINARY_FILE}';
11+
import source wasmModule from './{{{ WASM_BINARY_FILE }}}';
1212
#endif
1313

1414
#if ENVIRONMENT_MAY_BE_WEB && !EXPORT_ES6 && !(MINIMAL_RUNTIME && !PTHREADS)

src/postamble.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,9 @@ export default async function init(moduleArg = {}) {
306306
Object.assign(Module, moduleArg);
307307
processModuleArgs();
308308
#if WASM_ESM_INTEGRATION
309+
#if PTHREADS
310+
registerTLSInit(__emscripten_tls_init);
311+
#endif
309312
updateMemoryViews();
310313
#if DYNCALLS && '$dynCalls' in addedLibraryItems
311314

@@ -318,7 +321,7 @@ export default async function init(moduleArg = {}) {
318321
run();
319322
}
320323

321-
#if PTHREADS || WASM_WORKERS
324+
#if (WASM_WORKERS || PTHREADS) && !WASM_ESM_INTEGRATION
322325
// When run as a worker thread run `init` immediately.
323326
if ({{{ ENVIRONMENT_IS_WORKER_THREAD() }}}) await init()
324327
#endif

src/preamble.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -990,7 +990,7 @@ function getWasmImports() {
990990
#endif // WASM_ASYNC_COMPILATION
991991
#endif // SOURCE_PHASE_IMPORTS
992992
}
993-
#endif
993+
#endif // WASM_ESM_INTEGRATION
994994

995995
#if !WASM_BIGINT
996996
// Globals used by JS i64 conversions (see makeSetValue)

src/pthread_esm_startup.mjs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @license
3+
* Copyright 2025 The Emscripten Authors
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
// This file is used as the initial script loaded into pthread workers when
8+
// running in WASM_ESM_INTEGRATION mode.
9+
// Tyhe point of this file is to delay the loading of the main program module
10+
// until the wasm memory has been received via postMessage.
11+
12+
#if RUNTIME_DEBUG
13+
console.log("Running pthread_esm_startup");
14+
#endif
15+
16+
#if ENVIRONMENT_MAY_BE_NODE
17+
// Create as web-worker-like an environment as we can.
18+
var worker_threads = await import('worker_threads');
19+
global.Worker = worker_threads.Worker;
20+
var parentPort = worker_threads['parentPort'];
21+
parentPort.on('message', (msg) => global.onmessage?.({ data: msg }));
22+
Object.assign(globalThis, {
23+
self: global,
24+
postMessage: (msg) => parentPort['postMessage'](msg),
25+
});
26+
#endif
27+
28+
self.onmessage = async (msg) => {
29+
#if RUNTIME_DEBUG
30+
console.log('pthread_esm_startup', msg.data.cmd);
31+
#endif
32+
if (msg.data.cmd == 'load') {
33+
// Until we initialize the runtime, queue up any further incoming messages
34+
// that can arrive while the async import (await import below) is happening.
35+
// For examples the `run` message often arrives right away before the import
36+
// is complete.
37+
let messageQueue = [msg];
38+
self.onmessage = (e) => messageQueue.push(e);
39+
40+
// Now that we have the wasmMemory we can import the main program
41+
globalThis.wasmMemory = msg.data.wasmMemory;
42+
const prog = await import('./{{{ TARGET_JS_NAME }}}');
43+
44+
// Now that the import is completed the main program will have installed
45+
// its own `onmessage` handler and replaced our handler.
46+
// Now we can dispatch any queued messages to this new handler.
47+
for (let msg of messageQueue) {
48+
await self.onmessage(msg);
49+
}
50+
51+
await prog.default()
52+
}
53+
};

src/runtime_common.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ var readyPromiseResolve, readyPromiseReject;
2929
var wasmModuleReceived;
3030
#endif
3131

32-
#if ENVIRONMENT_MAY_BE_NODE
32+
#if ENVIRONMENT_MAY_BE_NODE && !WASM_ESM_INTEGRATION
3333
if (ENVIRONMENT_IS_NODE && {{{ ENVIRONMENT_IS_WORKER_THREAD() }}}) {
3434
// Create as web-worker-like an environment as we can.
3535
var parentPort = worker_threads['parentPort'];
@@ -39,7 +39,7 @@ if (ENVIRONMENT_IS_NODE && {{{ ENVIRONMENT_IS_WORKER_THREAD() }}}) {
3939
postMessage: (msg) => parentPort['postMessage'](msg),
4040
});
4141
}
42-
#endif // ENVIRONMENT_MAY_BE_NODE
42+
#endif // ENVIRONMENT_MAY_BE_NODE && !WASM_ESM_INTEGRATION
4343
#endif
4444

4545
#if PTHREADS

src/runtime_init_memory.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212
// check for full engine support (use string 'subarray' to avoid closure compiler confusion)
1313

1414
function initMemory() {
15+
#if WASM_ESM_INTEGRATION && PTHREADS
16+
if (ENVIRONMENT_IS_PTHREAD) {
17+
wasmMemory = globalThis.wasmMemory;
18+
assert(wasmMemory);
19+
updateMemoryViews();
20+
}
21+
#endif
22+
1523
{{{ runIfWorkerThread('return') }}}
1624

1725
#if expectToReceiveOnModule('wasmMemory')

src/runtime_pthread.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,10 @@ if (ENVIRONMENT_IS_PTHREAD) {
104104
#endif
105105
}
106106

107+
#if !WASM_ESM_INTEGRATION
107108
wasmMemory = msgData.wasmMemory;
108109
updateMemoryViews();
110+
#endif
109111

110112
#if LOAD_SOURCE_MAP
111113
wasmSourceMap = resetPrototype(WasmSourceMap, msgData.wasmSourceMap);
@@ -114,13 +116,15 @@ if (ENVIRONMENT_IS_PTHREAD) {
114116
wasmOffsetConverter = resetPrototype(WasmOffsetConverter, msgData.wasmOffsetConverter);
115117
#endif
116118

119+
#if !WASM_ESM_INTEGRATION
117120
#if MINIMAL_RUNTIME
118121
// Pass the shared Wasm module in the Module object for MINIMAL_RUNTIME.
119122
Module['wasm'] = msgData.wasmModule;
120123
loadModule();
121124
#else
122125
wasmModuleReceived(msgData.wasmModule);
123126
#endif // MINIMAL_RUNTIME
127+
#endif
124128
} else if (cmd === 'run') {
125129
#if ASSERTIONS
126130
assert(msgData.pthread_ptr);

test/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,8 @@ def also_with_modularize(f):
683683
@wraps(f)
684684
def metafunc(self, modularize, *args, **kwargs):
685685
if modularize:
686+
if '-sWASM_ESM_INTEGRATION':
687+
self.skipTest('also_with_modularize is not compatible with WASM_ESM_INTEGRATION')
686688
self.emcc_args += ['--extern-post-js', test_file('modularize_post_js.js'), '-sMODULARIZE']
687689
f(self, *args, **kwargs)
688690

@@ -1177,8 +1179,6 @@ def setup_node_pthreads(self):
11771179
self.emcc_args += ['-Wno-pthreads-mem-growth', '-pthread']
11781180
if self.get_setting('MINIMAL_RUNTIME'):
11791181
self.skipTest('node pthreads not yet supported with MINIMAL_RUNTIME')
1180-
if self.get_setting('WASM_ESM_INTEGRATION'):
1181-
self.skipTest('pthreads not yet supported with WASM_ESM_INTEGRATION')
11821182
nodejs = self.get_nodejs()
11831183
self.js_engines = [nodejs]
11841184
self.node_args += shared.node_pthread_flags(nodejs)

test/test_core.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7761,6 +7761,7 @@ def test_webidl(self, mode, allow_memory_growth):
77617761
# PTHREAD_POOL_DELAY_LOAD=1 adds a pthreadPoolReady promise that users
77627762
# can wait on for pthread initialization.
77637763
@node_pthreads
7764+
@no_esm_integration('WASM_ESM_INTEGRATION is not compatible with WASM_ASYNC_COMPILATION')
77647765
def test_embind_sync_if_pthread_delayed(self):
77657766
self.set_setting('WASM_ASYNC_COMPILATION', 0)
77667767
self.set_setting('PTHREAD_POOL_DELAY_LOAD', 1)
@@ -9283,6 +9284,7 @@ def test_pthread_unhandledrejection(self):
92839284
@node_pthreads
92849285
@no_wasm2js('wasm2js does not support PROXY_TO_PTHREAD (custom section support)')
92859286
@also_with_modularize
9287+
@no_esm_integration('USE_OFFSET_CONVERTER')
92869288
def test_pthread_offset_converter(self):
92879289
self.set_setting('PROXY_TO_PTHREAD')
92889290
self.set_setting('EXIT_RUNTIME')
@@ -9703,6 +9705,7 @@ def test_emscripten_async_load_script(self):
97039705
@no_sanitize('sanitizers do not support WASM_WORKERS')
97049706
@also_with_minimal_runtime
97059707
@also_with_modularize
9708+
@no_esm_integration('WASM_ESM_INTEGRATION is not compatible with WASM_WORKERS')
97069709
def test_wasm_worker_hello(self):
97079710
if self.is_wasm2js() and '-sMODULARIZE' in self.emcc_args:
97089711
self.skipTest('WASM2JS + MODULARIZE + WASM_WORKERS is not supported')
@@ -9711,11 +9714,13 @@ def test_wasm_worker_hello(self):
97119714

97129715
@node_pthreads
97139716
@no_sanitize('sanitizers do not support WASM_WORKERS')
9717+
@no_esm_integration('WASM_ESM_INTEGRATION is not compatible with WASM_WORKERS')
97149718
def test_wasm_worker_malloc(self):
97159719
self.do_run_in_out_file_test('wasm_worker/malloc_wasm_worker.c', emcc_args=['-sWASM_WORKERS'])
97169720

97179721
@node_pthreads
97189722
@no_sanitize('sanitizers do not support WASM_WORKERS')
9723+
@no_esm_integration('WASM_ESM_INTEGRATION is not compatible with WASM_WORKERS')
97199724
def test_wasm_worker_wait_async(self):
97209725
self.do_runf('atomic/test_wait_async.c', emcc_args=['-sWASM_WORKERS'])
97219726

tools/link.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -810,8 +810,8 @@ def phase_linker_setup(options, linker_args): # noqa: C901, PLR0912, PLR0915
810810
exit_with_error('WASM_ESM_INTEGRATION is not compatible with dynamic linking')
811811
if settings.ASYNCIFY:
812812
exit_with_error('WASM_ESM_INTEGRATION is not compatible with -sASYNCIFY')
813-
if settings.WASM_WORKERS or settings.PTHREADS:
814-
exit_with_error('WASM_ESM_INTEGRATION is not compatible with multi-threading')
813+
if settings.WASM_WORKERS:
814+
exit_with_error('WASM_ESM_INTEGRATION is not compatible with WASM_WORKERS')
815815
if settings.USE_OFFSET_CONVERTER:
816816
exit_with_error('WASM_ESM_INTEGRATION is not compatible with USE_OFFSET_CONVERTER')
817817
if not settings.WASM_ASYNC_COMPILATION:
@@ -2233,6 +2233,11 @@ def phase_final_emitting(options, target, js_target, wasm_target):
22332233
support_target = unsuffixed(js_target) + '.support.mjs'
22342234
move_file(final_js, support_target)
22352235
create_esm_wrapper(js_target, support_target, wasm_target)
2236+
if settings.PTHREADS:
2237+
support_target = unsuffixed(js_target) + '.pthread.mjs'
2238+
pthread_code = building.read_and_preprocess(utils.path_from_root('src/pthread_esm_startup.mjs'), expand_macros=True)
2239+
write_file(support_target, pthread_code)
2240+
fix_es6_import_statements(support_target)
22362241
else:
22372242
move_file(final_js, js_target)
22382243

0 commit comments

Comments
 (0)