Skip to content

Commit 6be9440

Browse files
authored
[esm-integration] Add pthread support (#24555)
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 db66f17 commit 6be9440

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')

0 commit comments

Comments
 (0)