Skip to content

Commit cf3c2d0

Browse files
authored
Fix JSPI suspend through call from side module to main module (#23581)
This fixes a basic use case where a side module calls a suspending main module symbol. Without this change, we call through a promising JS wrapper produced by `instrumentWasmExports()` and fail to suspend. We have to dlsym() the side module symbol since if we call it directly it will fail because of another JS trampoline on the main -> side module edge.
1 parent 3084681 commit cf3c2d0

File tree

7 files changed

+77
-15
lines changed

7 files changed

+77
-15
lines changed

src/lib/libdylink.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,14 @@ var LibraryDylink = {
716716
}
717717
if (prop in wasmImports && !wasmImports[prop].stub) {
718718
// No stub needed, symbol already exists in symbol table
719-
return wasmImports[prop];
719+
var res = wasmImports[prop];
720+
#if ASYNCIFY
721+
// Asyncify wraps exports, and we need to look through those wrappers.
722+
if (res.orig) {
723+
res = res.orig;
724+
}
725+
#endif
726+
return res;
720727
}
721728
// Return a stub function that will resolve the symbol
722729
// when first called.
@@ -1248,7 +1255,7 @@ var LibraryDylink = {
12481255

12491256
#if ASYNCIFY
12501257
// Asyncify wraps exports, and we need to look through those wrappers.
1251-
if ('orig' in result) {
1258+
if (result.orig) {
12521259
result = result.orig;
12531260
}
12541261
#endif

test/common.py

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

323323

324+
def requires_jspi(func):
325+
assert callable(func)
326+
327+
@wraps(func)
328+
def decorated(self, *args, **kwargs):
329+
self.require_jspi()
330+
return func(self, *args, **kwargs)
331+
332+
return decorated
333+
334+
324335
def node_pthreads(f):
325336
assert callable(f)
326337

test/core/test_dlfcn_jspi.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2025 The Emscripten Authors. All rights reserved.
2+
// Emscripten is available under two separate licenses, the MIT license and the
3+
// University of Illinois/NCSA Open Source License. Both these licenses can be
4+
// found in the LICENSE file.
5+
6+
#include <emscripten.h>
7+
#include <stdio.h>
8+
#include <dlfcn.h>
9+
10+
EM_ASYNC_JS(int, test, (), {
11+
console.log("sleeping");
12+
await new Promise(res => setTimeout(res, 0));
13+
console.log("slept");
14+
return 77;
15+
});
16+
17+
int test_wrapper() {
18+
return test();
19+
}
20+
21+
typedef int (*F)();
22+
23+
int main() {
24+
void* handle = dlopen("side.so", RTLD_NOW|RTLD_GLOBAL);
25+
F side_module_trampoline = dlsym(handle, "side_module_trampoline");
26+
int res = side_module_trampoline();
27+
printf("done %d\n", res);
28+
return 0;
29+
}

test/core/test_dlfcn_jspi.out

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
side_module_trampoline
2+
sleeping
3+
slept
4+
done 77

test/core/test_dlfcn_jspi_side.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include <stdio.h>
2+
int test_wrapper(void);
3+
4+
int side_module_trampoline() {
5+
printf("side_module_trampoline\n");
6+
return test_wrapper();
7+
}

test/test_core.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from common import read_file, read_binary, requires_v8, requires_node, requires_wasm2js, requires_node_canary
2929
from common import compiler_for, crossplatform, no_4gb, no_2gb, also_with_minimal_runtime, also_with_modularize
3030
from common import with_all_fs, also_with_nodefs, also_with_nodefs_both, also_with_noderawfs, also_with_wasmfs
31-
from common import with_all_eh_sjlj, with_all_sjlj, also_with_standalone_wasm, can_do_standalone, no_wasm64, requires_wasm_eh
31+
from common import with_all_eh_sjlj, with_all_sjlj, also_with_standalone_wasm, can_do_standalone, no_wasm64, requires_wasm_eh, requires_jspi
3232
from common import NON_ZERO, WEBIDL_BINDER, EMBUILDER, PYTHON
3333
import clang_native
3434

@@ -3762,6 +3762,21 @@ def test_dlfcn_asyncify(self):
37623762
'''
37633763
self.do_run(src, 'before sleep\nafter sleep\n42\n')
37643764

3765+
@requires_jspi
3766+
@needs_dylink
3767+
def test_dlfcn_jspi(self):
3768+
self.run_process(
3769+
[
3770+
EMCC,
3771+
"-o",
3772+
"side.so",
3773+
test_file("core/test_dlfcn_jspi_side.c"),
3774+
"-sSIDE_MODULE",
3775+
]
3776+
+ self.get_emcc_args()
3777+
)
3778+
self.do_run_in_out_file_test("core/test_dlfcn_jspi.c", emcc_args=["side.so", "-sMAIN_MODULE=2"])
3779+
37653780
@needs_dylink
37663781
def test_dlfcn_rtld_local(self):
37673782
# Create two shared libraries that both depend on a third.

test/test_other.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from common import env_modify, no_mac, no_windows, only_windows, requires_native_clang, with_env_modify
3636
from common import create_file, parameterized, NON_ZERO, node_pthreads, TEST_ROOT, test_file
3737
from common import compiler_for, EMBUILDER, requires_v8, requires_node, requires_wasm64, requires_node_canary
38-
from common import requires_wasm_eh, crossplatform, with_all_eh_sjlj, with_all_sjlj
38+
from common import requires_wasm_eh, crossplatform, with_all_eh_sjlj, with_all_sjlj, requires_jspi
3939
from common import also_with_standalone_wasm, also_with_wasm2js, also_with_noderawfs
4040
from common import also_with_modularize, also_with_wasmfs, with_all_fs
4141
from common import also_with_minimal_runtime, also_with_wasm_bigint, also_with_wasm64, also_with_asan, flaky
@@ -195,17 +195,6 @@ def decorated(self, *args, **kwargs):
195195
return decorated
196196

197197

198-
def requires_jspi(func):
199-
assert callable(func)
200-
201-
@wraps(func)
202-
def decorated(self, *args, **kwargs):
203-
self.require_jspi()
204-
return func(self, *args, **kwargs)
205-
206-
return decorated
207-
208-
209198
def llvm_nm(file):
210199
output = shared.run_process([LLVM_NM, file], stdout=PIPE).stdout
211200

0 commit comments

Comments
 (0)