Skip to content

Commit 575eb75

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 f21d1f5 commit 575eb75

File tree

12 files changed

+119
-30
lines changed

12 files changed

+119
-30
lines changed

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: 25 additions & 13 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.
@@ -282,6 +289,7 @@ var LibraryPThread = {
282289
message = `Pthread ${ptrToString(worker.pthread_ptr)} sent an error!`;
283290
}
284291
#endif
292+
console.error(e.stack);
285293
err(`${message} ${e.filename}:${e.lineno}: ${e.message}`);
286294
throw e;
287295
};
@@ -295,7 +303,9 @@ var LibraryPThread = {
295303

296304
#if ASSERTIONS
297305
assert(wasmMemory instanceof WebAssembly.Memory, 'WebAssembly memory should have been loaded by now!');
306+
#if !WASM_ESM_INTEGRATION
298307
assert(wasmModule instanceof WebAssembly.Module, 'WebAssembly Module should have been loaded by now!');
308+
#endif
299309
#endif
300310

301311
// When running on a pthread, none of the incoming parameters on the module
@@ -333,7 +343,9 @@ var LibraryPThread = {
333343
#else // WASM2JS
334344
wasmMemory,
335345
#endif // WASM2JS
346+
#if !WASM_ESM_INTEGRATION
336347
wasmModule,
348+
#endif
337349
#if LOAD_SOURCE_MAP
338350
wasmSourceMap,
339351
#endif
@@ -391,25 +403,25 @@ var LibraryPThread = {
391403
#if TRUSTED_TYPES
392404
// Use Trusted Types compatible wrappers.
393405
if (typeof trustedTypes != 'undefined' && trustedTypes.createPolicy) {
394-
var p = trustedTypes.createPolicy('emscripten#workerPolicy1', { createScriptURL: (ignored) => import.meta.url });
406+
var p = trustedTypes.createPolicy('emscripten#workerPolicy1', { createScriptURL: (ignored) => new URL('{{{ pthreadWorkerScript }}}', import.meta.url));
395407
worker = new Worker(p.createScriptURL('ignored'), {{{ pthreadWorkerOptions }}});
396408
} else
397409
#endif
398410
#if expectToReceiveOnModule('mainScriptUrlOrBlob')
399-
if (Module['mainScriptUrlOrBlob']) {
400-
var pthreadMainJs = Module['mainScriptUrlOrBlob'];
401-
if (typeof pthreadMainJs != 'string') {
402-
pthreadMainJs = URL.createObjectURL(pthreadMainJs);
403-
}
404-
worker = new Worker(pthreadMainJs, {{{ pthreadWorkerOptions }}});
405-
} else
411+
if (Module['mainScriptUrlOrBlob']) {
412+
var pthreadMainJs = Module['mainScriptUrlOrBlob'];
413+
if (typeof pthreadMainJs != 'string') {
414+
pthreadMainJs = URL.createObjectURL(pthreadMainJs);
415+
}
416+
worker = new Worker(pthreadMainJs, {{{ pthreadWorkerOptions }}});
417+
} else
406418
#endif
407419
// We need to generate the URL with import.meta.url as the base URL of the JS file
408420
// instead of just using new URL(import.meta.url) because bundler's only recognize
409421
// the first case in their bundling step. The latter ends up producing an invalid
410422
// URL to import from the server (e.g., for webpack the file:// path).
411423
// See https://github.com/webpack/webpack/issues/12638
412-
worker = new Worker(new URL('{{{ TARGET_JS_NAME }}}', import.meta.url), {{{ pthreadWorkerOptions }}});
424+
worker = new Worker(new URL('{{{ pthreadWorkerScript }}}', import.meta.url), {{{ pthreadWorkerOptions }}});
413425
#else // EXPORT_ES6
414426
var pthreadMainJs = _scriptName;
415427
#if expectToReceiveOnModule('mainScriptUrlOrBlob')
@@ -601,10 +613,10 @@ var LibraryPThread = {
601613

602614
worker.pthread_ptr = threadParams.pthread_ptr;
603615
var msg = {
604-
cmd: 'run',
605-
start_routine: threadParams.startRoutine,
606-
arg: threadParams.arg,
607-
pthread_ptr: threadParams.pthread_ptr,
616+
cmd: 'run',
617+
start_routine: threadParams.startRoutine,
618+
arg: threadParams.arg,
619+
pthread_ptr: threadParams.pthread_ptr,
608620
};
609621
#if OFFSCREENCANVAS_SUPPORT
610622
// Note that we do not need to quote these names because they are only used

src/postamble.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
// === Auto-generated postamble setup entry stuff ===
88

9+
#include "runtime_common.js"
10+
911
#if PROXY_TO_WORKER
1012
if (ENVIRONMENT_IS_WORKER) {
1113
#include "webGLWorker.js'
@@ -306,6 +308,9 @@ export default async function init(moduleArg = {}) {
306308
Object.assign(Module, moduleArg);
307309
processModuleArgs();
308310
#if WASM_ESM_INTEGRATION
311+
#if PTHREADS
312+
registerTLSInit(__emscripten_tls_init);
313+
#endif
309314
updateMemoryViews();
310315
#if DYNCALLS && '$dynCalls' in addedLibraryItems
311316

@@ -318,9 +323,9 @@ export default async function init(moduleArg = {}) {
318323
run();
319324
}
320325

321-
#if PTHREADS || WASM_WORKERS
326+
#if WASM_WORKERS
322327
// When run as a worker thread run `init` immediately.
323-
if ({{{ ENVIRONMENT_IS_WORKER_THREAD() }}}) await init()
328+
if ({{{ ENVIRONMENT_IS_WORKER_THREAD() }}}) init()
324329
#endif
325330

326331
#if ENVIRONMENT_MAY_BE_NODE

src/preamble.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,6 @@ function _free() {
101101
*/
102102
var isFileURI = (filename) => filename.startsWith('file://');
103103

104-
#include "runtime_common.js"
105-
106104
#if ASSERTIONS
107105
assert(typeof Int32Array != 'undefined' && typeof Float64Array !== 'undefined' && Int32Array.prototype.subarray != undefined && Int32Array.prototype.set != undefined,
108106
'JS engine does not provide full typed array support');
@@ -144,7 +142,7 @@ function initRuntime() {
144142
#endif
145143

146144
#if PTHREADS
147-
if (ENVIRONMENT_IS_PTHREAD) return startWorker(Module);
145+
if (ENVIRONMENT_IS_PTHREAD) return startWorker();
148146
#endif
149147

150148
#if STACK_OVERFLOW_CHECK >= 2
@@ -990,7 +988,7 @@ function getWasmImports() {
990988
#endif // WASM_ASYNC_COMPILATION
991989
#endif // SOURCE_PHASE_IMPORTS
992990
}
993-
#endif
991+
#endif // WASM_ESM_INTEGRATION
994992

995993
#if !WASM_BIGINT
996994
// Globals used by JS i64 conversions (see makeSetValue)

src/pthread_esm_startup.mjs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
// The point of this file is to delay the loading 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+
let messageQueue = [msg];
35+
self.onmessage = (e) => messageQueue.push(e);
36+
37+
// Now that we have the wasmMemory we can import the main program
38+
globalThis.wasmMemory = msg.data.wasmMemory;
39+
const prog = await import('./{{{ TARGET_JS_NAME }}}');
40+
41+
// Dispatch queued messages the new handler installed by the main program
42+
for (let msg of messageQueue) {
43+
await self.onmessage(msg);
44+
}
45+
46+
await prog.default()
47+
}
48+
};

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: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,23 @@ if (ENVIRONMENT_IS_PTHREAD) {
4545
var msgData = e['data'];
4646
//dbg('msgData: ' + Object.keys(msgData));
4747
var cmd = msgData.cmd;
48+
#if PTHREADS_DEBUG
49+
dbg('worker: got cmd:', cmd)
50+
#endif
4851
if (cmd === 'load') { // Preload command that is called once per worker to parse and load the Emscripten code.
4952
#if ASSERTIONS
5053
workerID = msgData.workerID;
5154
#endif
55+
5256
#if PTHREADS_DEBUG
5357
dbg('worker: loading module')
5458
#endif
55-
5659
// Until we initialize the runtime, queue up any further incoming messages.
5760
let messageQueue = [];
5861
self.onmessage = (e) => messageQueue.push(e);
5962

6063
// And add a callback for when the runtime is initialized.
61-
self.startWorker = (instance) => {
64+
self.startWorker = () => {
6265
// Notify the main thread that this thread has loaded.
6366
postMessage({ cmd: 'loaded' });
6467
// Process any messages that were queued before the thread was ready.
@@ -102,8 +105,10 @@ if (ENVIRONMENT_IS_PTHREAD) {
102105
#endif
103106
}
104107

108+
#if !WASM_ESM_INTEGRATION
105109
wasmMemory = msgData.wasmMemory;
106110
updateMemoryViews();
111+
#endif
107112

108113
#if LOAD_SOURCE_MAP
109114
wasmSourceMap = resetPrototype(WasmSourceMap, msgData.wasmSourceMap);
@@ -112,13 +117,15 @@ if (ENVIRONMENT_IS_PTHREAD) {
112117
wasmOffsetConverter = resetPrototype(WasmOffsetConverter, msgData.wasmOffsetConverter);
113118
#endif
114119

120+
#if !WASM_ESM_INTEGRATION
115121
#if MINIMAL_RUNTIME
116122
// Pass the shared Wasm module in the Module object for MINIMAL_RUNTIME.
117123
Module['wasm'] = msgData.wasmModule;
118124
loadModule();
119125
#else
120126
wasmModuleReceived(msgData.wasmModule);
121127
#endif // MINIMAL_RUNTIME
128+
#endif
122129
} else if (cmd === 'run') {
123130
#if ASSERTIONS
124131
assert(msgData.pthread_ptr);

test/common.py

Lines changed: 2 additions & 3 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('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

@@ -1125,7 +1127,6 @@ def require_jspi(self):
11251127
self.skipTest('JSPI is not currently supported for WASM2JS')
11261128
if self.get_setting('WASM_ESM_INTEGRATION'):
11271129
self.skipTest('WASM_ESM_INTEGRATION is not compatible with JSPI')
1128-
11291130
if self.is_browser_test():
11301131
if 'EMTEST_SKIP_JSPI' in os.environ:
11311132
self.skipTest('skipping JSPI (EMTEST_SKIP_JSPI is set)')
@@ -1177,8 +1178,6 @@ def setup_node_pthreads(self):
11771178
self.emcc_args += ['-Wno-pthreads-mem-growth', '-pthread']
11781179
if self.get_setting('MINIMAL_RUNTIME'):
11791180
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')
11821181
nodejs = self.get_nodejs()
11831182
self.js_engines = [nodejs]
11841183
self.node_args += shared.node_pthread_flags(nodejs)

test/core/test_pthread_join_and_asyncify.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
// University of Illinois/NCSA Open Source License. Both these licenses can be
44
// found in the LICENSE file.
55

6-
#include <emscripten.h>
6+
#include <emscripten/emscripten.h>
7+
#include <emscripten/console.h>
78
#include <pthread.h>
89
#include <stdio.h>
910
#include <assert.h>
@@ -15,6 +16,7 @@ EM_ASYNC_JS(int, async_call, (), {
1516

1617
void *run_thread(void *args) {
1718
int ret = async_call();
19+
emscripten_outf("done async_call: %d", ret);
1820
assert(ret == 42);
1921
return NULL;
2022
}

0 commit comments

Comments
 (0)