Skip to content

Commit ab5c5c6

Browse files
committed
Fix multithreading support for Deno and Bun
Fixes denoland/deno#17171 Also adds several pthread tests with Bun and Deno
1 parent cb44503 commit ab5c5c6

File tree

10 files changed

+127
-27
lines changed

10 files changed

+127
-27
lines changed

.circleci/config.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,34 @@ jobs:
883883
echo "BUN_ENGINE = os.path.expanduser('~/.bun/bin/bun')" >> ~/emsdk/.emscripten
884884
echo "JS_ENGINES = [BUN_ENGINE]" >> ~/emsdk/.emscripten
885885
- run-tests:
886-
test_targets: "core0.test_hello_world"
886+
test_targets: "
887+
core0.test_hello_world
888+
core0.test_hello_argc_pthreads
889+
core2.test_pthread_create
890+
core2.test_pthread_unhandledrejection
891+
"
892+
test-deno:
893+
executor: linux-python
894+
steps:
895+
- checkout
896+
- pip-install
897+
- install-emsdk
898+
- run:
899+
name: install deno
900+
command: |
901+
curl -fsSL https://deno.land/install.sh | sh
902+
echo "DENO_ENGINE = [os.path.expanduser('~/.deno/bin/deno'), 'run', '--allow-read', '--unstable-detect-cjs']" >> ~/emsdk/.emscripten
903+
echo "JS_ENGINES = [DENO_ENGINE]" >> ~/emsdk/.emscripten
904+
- run-tests:
905+
test_targets: "
906+
core0.test_hello_world
907+
core0.test_asyncify_main_module
908+
core2.test_pthread_create
909+
core2.test_pthread_unhandledrejection
910+
"
911+
# The following tests using PROXY_TO_PTHREAD are disabled because of https://github.com/denoland/deno/issues/14786:
912+
# core0.test_hello_argc_pthreads
913+
# core0.test_pthread_join_and_asyncify
887914
test-jsc:
888915
executor: linux-python
889916
steps:
@@ -1377,6 +1404,7 @@ workflows:
13771404
requires:
13781405
- build-linux
13791406
- test-bun
1407+
- test-deno
13801408
- test-jsc
13811409
- test-spidermonkey
13821410
- test-node-compat

src/pthread_esm_startup.mjs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@ console.log("Running pthread_esm_startup");
1515

1616
#if ENVIRONMENT_MAY_BE_NODE
1717
// 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-
});
18+
globalThis.self = globalThis;
19+
// Deno and Bun already have `postMessage` defined on the global scope and
20+
// deliver messages to `globalThis.onmessage`, so we must not duplicate that
21+
// behavior here if `postMessage` is already present.
22+
if (!globalThis.postMessage) {
23+
const worker_threads = await import('worker_threads');
24+
globalThis.Worker = worker_threads.Worker;
25+
const parentPort = worker_threads['parentPort'];
26+
parentPort.on('message', (msg) => globalThis.onmessage?.({ data: msg }));
27+
globalThis.postMessage = (msg) => parentPort['postMessage'](msg);
28+
}
2629
#endif
2730

2831
self.onmessage = async (msg) => {

src/runtime_common.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@ var readyPromiseResolve, readyPromiseReject;
3838
#if (PTHREADS || WASM_WORKERS) && (ENVIRONMENT_MAY_BE_NODE && !WASM_ESM_INTEGRATION)
3939
if (ENVIRONMENT_IS_NODE && {{{ ENVIRONMENT_IS_WORKER_THREAD() }}}) {
4040
// Create as web-worker-like an environment as we can.
41-
var parentPort = worker_threads['parentPort'];
42-
parentPort.on('message', (msg) => global.onmessage?.({ data: msg }));
43-
Object.assign(globalThis, {
44-
self: global,
45-
postMessage: (msg) => parentPort['postMessage'](msg),
46-
});
41+
globalThis.self = globalThis;
42+
// Deno and Bun already have `postMessage` defined on the global scope and
43+
// deliver messages to `globalThis.onmessage`, so we must not duplicate that
44+
// behavior here if `postMessage` is already present.
45+
if (!globalThis.postMessage) {
46+
const parentPort = worker_threads['parentPort'];
47+
parentPort.on('message', (msg) => globalThis.onmessage?.({ data: msg }));
48+
globalThis.postMessage = (msg) => parentPort['postMessage'](msg);
49+
}
4750
// Node.js Workers do not pass postMessage()s and uncaught exception events to the parent
4851
// thread necessarily in the same order where they were generated in sequential program order.
4952
// See https://github.com/nodejs/node/issues/59617

src/wasm_worker.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ if (ENVIRONMENT_IS_NODE) {
5757
return f;
5858
}
5959

60+
const parentPort = worker_threads['parentPort'];
6061
Object.assign(globalThis, {
6162
addEventListener: (name, handler) => parentPort['on'](name, wrapMsgHandler(handler)),
6263
removeEventListener: (name, handler) => parentPort['off'](name, wrapMsgHandler(handler)),

test/common.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,31 @@ def require_node(self):
393393
self.require_engine(nodejs)
394394
return nodejs
395395

396+
def get_deno(self):
397+
"""Return deno engine, if one is configured, otherwise None"""
398+
if config.DENO_ENGINE and config.DENO_ENGINE in config.JS_ENGINES:
399+
return config.DENO_ENGINE
400+
return None
401+
402+
def get_node_bun_or_deno(self):
403+
"""Return nodejs, bun, or deno engine, if one is configured, otherwise None"""
404+
if config.NODE_JS_TEST in config.JS_ENGINES:
405+
return config.NODE_JS_TEST
406+
if config.BUN_ENGINE and config.BUN_ENGINE in config.JS_ENGINES:
407+
return config.BUN_ENGINE
408+
if config.DENO_ENGINE and config.DENO_ENGINE in config.JS_ENGINES:
409+
return config.DENO_ENGINE
410+
return None
411+
412+
def require_node_bun_or_deno(self):
413+
if 'EMTEST_SKIP_NODE' in os.environ:
414+
self.skipTest('test requires node, bun, or deno and EMTEST_SKIP_NODE is set')
415+
engine = self.get_node_bun_or_deno()
416+
if not engine:
417+
self.fail('node, bun, or deno required to run this test. Use EMTEST_SKIP_NODE to skip')
418+
self.require_engine(engine)
419+
return engine
420+
396421
def node_is_canary(self, nodejs):
397422
return nodejs and nodejs[0] and ('canary' in nodejs[0] or 'nightly' in nodejs[0])
398423

@@ -406,6 +431,20 @@ def require_node_canary(self):
406431

407432
self.fail('node canary required to run this test. Use EMTEST_SKIP_NODE_CANARY to skip')
408433

434+
def require_node_canary_or_deno(self):
435+
if 'EMTEST_SKIP_NODE_CANARY' in os.environ:
436+
self.skipTest('test requires node canary or deno and EMTEST_SKIP_NODE_CANARY is set')
437+
nodejs = self.get_nodejs()
438+
if self.node_is_canary(nodejs):
439+
self.require_engine(nodejs)
440+
return
441+
deno = self.get_deno()
442+
if deno:
443+
self.require_engine(deno)
444+
return
445+
446+
self.fail('node canary or deno required to run this test. Use EMTEST_SKIP_NODE_CANARY to skip')
447+
409448
def require_engine(self, engine):
410449
logger.debug(f'require_engine: {engine}')
411450
if self.required_engine and self.required_engine != engine:
@@ -517,9 +556,16 @@ def require_jspi(self):
517556
return
518557

519558
exp_args = ['--experimental-wasm-stack-switching', '--experimental-wasm-type-reflection']
520-
# Support for JSPI came earlier than 22, but the new API changes require v24
521-
if self.try_require_node_version(24):
522-
self.node_args += exp_args
559+
nodejs = self.get_nodejs()
560+
if nodejs:
561+
# Support for JSPI came earlier than 22, but the new API changes require v24
562+
if self.try_require_node_version(24):
563+
self.node_args += exp_args
564+
return
565+
566+
deno = self.get_deno()
567+
if deno:
568+
self.js_engines = [deno]
523569
return
524570

525571
v8 = self.get_v8()
@@ -529,7 +575,7 @@ def require_jspi(self):
529575
self.v8_args += exp_args
530576
return
531577

532-
self.fail('either d8 or node v24 required to run JSPI tests. Use EMTEST_SKIP_JSPI to skip')
578+
self.fail('either d8, node v24, or deno required to run JSPI tests. Use EMTEST_SKIP_JSPI to skip')
533579

534580
def require_wasm2js(self):
535581
if self.is_wasm64():
@@ -556,13 +602,15 @@ def setup_wasmfs_test(self):
556602
self.cflags += ['-DWASMFS']
557603

558604
def setup_node_pthreads(self):
559-
self.require_node()
605+
self.require_node_bun_or_deno()
560606
self.cflags += ['-Wno-pthreads-mem-growth', '-pthread']
561607
if self.get_setting('MINIMAL_RUNTIME'):
562608
self.skipTest('node pthreads not yet supported with MINIMAL_RUNTIME')
609+
engine = self.get_node_bun_or_deno()
610+
self.js_engines = [engine]
563611
nodejs = self.get_nodejs()
564-
self.js_engines = [nodejs]
565-
self.node_args += shared.node_pthread_flags(nodejs)
612+
if nodejs:
613+
self.node_args += shared.node_pthread_flags(engine)
566614

567615
def set_temp_dir(self, temp_dir):
568616
self.temp_dir = temp_dir

test/decorators.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ def decorated(self, *args, **kwargs):
140140
return decorated
141141

142142

143+
def requires_node_canary_or_deno(func):
144+
assert callable(func)
145+
146+
@wraps(func)
147+
def decorated(self, *args, **kwargs):
148+
self.require_node_canary_or_deno()
149+
return func(self, *args, **kwargs)
150+
151+
return decorated
152+
153+
143154
# Used to mark dependencies in various tests to npm developer dependency
144155
# packages, which might not be installed on Emscripten end users' systems.
145156
def requires_dev_dependency(package):
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#include <emscripten/emscripten.h>
22

33
int main() {
4-
EM_ASM({ Promise.reject("rejected!"); });
4+
EM_ASM({ Promise.reject(new Error("rejected!")); });
55
emscripten_exit_with_live_runtime();
66
}

test/test_browser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5401,7 +5401,7 @@ def test_pthread_unhandledrejection(self):
54015401
cflags=['-pthread', '-sPROXY_TO_PTHREAD', '--post-js',
54025402
test_file('pthread/test_pthread_unhandledrejection.post.js')],
54035403
# Firefox and Chrome report this slightly differently
5404-
expected=['exception:Uncaught rejected!', 'exception:uncaught exception: rejected!'])
5404+
expected=['exception:Error: rejected!', 'exception:Uncaught Error: rejected!'])
54055405

54065406
def test_pthread_key_recreation(self):
54075407
self.btest_exit('pthread/test_pthread_key_recreation.c', cflags=['-pthread', '-sPTHREAD_POOL_SIZE=1'])

test/test_core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
requires_jspi,
6161
requires_native_clang,
6262
requires_node,
63-
requires_node_canary,
63+
requires_node_canary_or_deno,
6464
requires_v8,
6565
requires_wasm2js,
6666
requires_wasm_eh,
@@ -8502,7 +8502,7 @@ def test_asyncify_main_module(self):
85028502
self.do_core_test('test_hello_world.c')
85038503

85048504
# Test that pthread_join works correctly with asyncify.
8505-
@requires_node_canary
8505+
@requires_node_canary_or_deno
85068506
@node_pthreads
85078507
def test_pthread_join_and_asyncify(self):
85088508
# TODO Test with ASYNCIFY=1 https://github.com/emscripten-core/emscripten/issues/17552

tools/config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
# See parse_config_file below.
2020
EMSCRIPTEN_ROOT = __rootpath__
2121
NODE_JS = None
22+
BUN_ENGINE = None
23+
DENO_ENGINE = None
2224
BINARYEN_ROOT = None
2325
LLVM_ADD_VERSION = None
2426
CLANG_ADD_VERSION = None
@@ -59,11 +61,13 @@ def fix_js_engine(old, new):
5961

6062
def normalize_config_settings():
6163
global CACHE, PORTS, LLVM_ADD_VERSION, CLANG_ADD_VERSION, CLOSURE_COMPILER
62-
global NODE_JS, NODE_JS_TEST, V8_ENGINE, JS_ENGINES, SPIDERMONKEY_ENGINE, WASM_ENGINES
64+
global NODE_JS, NODE_JS_TEST, BUN_ENGINE, DENO_ENGINE, V8_ENGINE, JS_ENGINES, SPIDERMONKEY_ENGINE, WASM_ENGINES
6365

6466
SPIDERMONKEY_ENGINE = fix_js_engine(SPIDERMONKEY_ENGINE, listify(SPIDERMONKEY_ENGINE))
6567
NODE_JS = fix_js_engine(NODE_JS, listify(NODE_JS))
6668
NODE_JS_TEST = fix_js_engine(NODE_JS_TEST, listify(NODE_JS_TEST))
69+
BUN_ENGINE = fix_js_engine(BUN_ENGINE, listify(BUN_ENGINE))
70+
DENO_ENGINE = fix_js_engine(DENO_ENGINE, listify(DENO_ENGINE))
6771
V8_ENGINE = fix_js_engine(V8_ENGINE, listify(V8_ENGINE))
6872
JS_ENGINES = [listify(engine) for engine in JS_ENGINES]
6973
WASM_ENGINES = [listify(engine) for engine in WASM_ENGINES]
@@ -102,6 +106,8 @@ def parse_config_file():
102106
CONFIG_KEYS = (
103107
'NODE_JS',
104108
'NODE_JS_TEST',
109+
'BUN_ENGINE',
110+
'DENO_ENGINE',
105111
'BINARYEN_ROOT',
106112
'SPIDERMONKEY_ENGINE',
107113
'V8_ENGINE',

0 commit comments

Comments
 (0)