From 0baf855459ee0c1f5cb7f77e7f88ebb226d5eeaf Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Mon, 21 Apr 2025 17:18:33 -0400 Subject: [PATCH 1/2] Fix late-binding symbols with JSPI (implementation 2) Late-binding symbols get a JS stub import that resolves the symbol and then makes an onward call. This breaks JSPI. This is a second approach to solving the problem by using WebAssembly.promising and WebAssembly.Suspending with a JS trampoline. Unfortunately, as far as I can tell there is no way to make it work for both a promising entrypoint and a non-promising entrypoint. This is due to a change in the JSPI spec: early versions said that if a suspending import does not return a promise, the suspender was allowed to be null. However, the new version of JSPI eagerly traps if the suspender is null, even if the function does not return a promise. --- src/lib/libdylink.js | 14 ++++++ test/core/test_dlfcn_jspi.c | 44 ++++++++++++++++--- test/core/test_dlfcn_jspi.out | 3 +- test/core/test_dlfcn_jspi_side_a.c | 10 +++++ test/core/test_dlfcn_jspi_side_b.c | 8 ++++ .../test_codesize_hello_dylink.gzsize | 2 +- .../test_codesize_hello_dylink.jssize | 2 +- test/test_core.py | 23 +++++++--- 8 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 test/core/test_dlfcn_jspi_side_a.c create mode 100644 test/core/test_dlfcn_jspi_side_b.c diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index b1e70178286f6..ac6ee32f2d6b3 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -685,6 +685,9 @@ var LibraryDylink = { if (!resolved) { resolved = moduleExports[sym]; } + if (resolved.orig) { + resolved = resolved.orig; + } #if ASSERTIONS assert(resolved, `undefined symbol '${sym}'. perhaps a side module was not linked in? if this global was expected to arrive from a system library, try to build the MAIN_MODULE with EMCC_FORCE_STDLIBS=1 in the environment`); #endif @@ -734,10 +737,21 @@ var LibraryDylink = { // when first called. if (!(prop in stubs)) { var resolved; +#if JSPI + var func = (...args) => { + resolved ||= resolveSymbol(prop); + try { + resolved = WebAssembly.promising(resolved); + } catch(e) {} + return resolved(...args); + } + stubs[prop] = new WebAssembly.Suspending(func); +#else stubs[prop] = (...args) => { resolved ||= resolveSymbol(prop); return resolved(...args); }; +#endif } return stubs[prop]; } diff --git a/test/core/test_dlfcn_jspi.c b/test/core/test_dlfcn_jspi.c index 0c73aa6250fb7..9238a8f0bdd38 100644 --- a/test/core/test_dlfcn_jspi.c +++ b/test/core/test_dlfcn_jspi.c @@ -3,27 +3,57 @@ // University of Illinois/NCSA Open Source License. Both these licenses can be // found in the LICENSE file. +#include #include #include #include -EM_ASYNC_JS(int, test, (), { +EM_ASYNC_JS(int, test_suspending, (), { console.log("sleeping"); await new Promise(res => setTimeout(res, 0)); console.log("slept"); return 77; }); -int test_wrapper() { - return test(); +int test_suspending_wrapper() { + return test_suspending(); } +EM_JS(int, test_sync, (), { + console.log("sync"); + return 77; +}) + +int test_sync_wrapper() { + return test_sync(); +} + + typedef int (*F)(); +typedef int (*G)(F f); + +void helper(F f) { + void* handle = dlopen("side_a.so", RTLD_NOW|RTLD_GLOBAL); + assert(handle != NULL); + G side_module_trampolinea = dlsym(handle, "side_module_trampoline_a"); + assert(side_module_trampolinea != NULL); + int res = side_module_trampolinea(f); + printf("okay %d\n", res); +} + + +EMSCRIPTEN_KEEPALIVE void not_promising() { + helper(test_sync_wrapper); +} + +EM_JS(void, js_trampoline, (), { + _not_promising(); +}) int main() { - void* handle = dlopen("side.so", RTLD_NOW|RTLD_GLOBAL); - F side_module_trampoline = dlsym(handle, "side_module_trampoline"); - int res = side_module_trampoline(); - printf("done %d\n", res); + helper(test_suspending_wrapper); + js_trampoline(); + printf("done\n"); return 0; } + diff --git a/test/core/test_dlfcn_jspi.out b/test/core/test_dlfcn_jspi.out index 8daf1b9e7f8bf..2b184530b264d 100644 --- a/test/core/test_dlfcn_jspi.out +++ b/test/core/test_dlfcn_jspi.out @@ -1,4 +1,5 @@ -side_module_trampoline +side_module_trampoline_a +side_module_trampoline_b sleeping slept done 77 diff --git a/test/core/test_dlfcn_jspi_side_a.c b/test/core/test_dlfcn_jspi_side_a.c new file mode 100644 index 0000000000000..ae9c04be15735 --- /dev/null +++ b/test/core/test_dlfcn_jspi_side_a.c @@ -0,0 +1,10 @@ +#include + +typedef int (*F)(); + +int side_module_trampoline_b(F f); + +int side_module_trampoline_a(F f) { + printf("side_module_trampoline_a\n"); + return side_module_trampoline_b(f); +} diff --git a/test/core/test_dlfcn_jspi_side_b.c b/test/core/test_dlfcn_jspi_side_b.c new file mode 100644 index 0000000000000..16354428cd417 --- /dev/null +++ b/test/core/test_dlfcn_jspi_side_b.c @@ -0,0 +1,8 @@ +#include + +typedef int (*F)(); + +int side_module_trampoline_b(F f) { + printf("side_module_trampoline_b\n"); + return f(); +} diff --git a/test/other/codesize/test_codesize_hello_dylink.gzsize b/test/other/codesize/test_codesize_hello_dylink.gzsize index a5755921eac44..27dfac602e314 100644 --- a/test/other/codesize/test_codesize_hello_dylink.gzsize +++ b/test/other/codesize/test_codesize_hello_dylink.gzsize @@ -1 +1 @@ -11735 +11736 diff --git a/test/other/codesize/test_codesize_hello_dylink.jssize b/test/other/codesize/test_codesize_hello_dylink.jssize index c82e0d84da885..7cb5c3b2b7ba6 100644 --- a/test/other/codesize/test_codesize_hello_dylink.jssize +++ b/test/other/codesize/test_codesize_hello_dylink.jssize @@ -1 +1 @@ -27774 +27776 diff --git a/test/test_core.py b/test/test_core.py index 7addc63783af4..b48a88e4fefcf 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -3789,13 +3789,24 @@ def test_dlfcn_jspi(self): self.run_process( [ EMCC, - "-o", - "side.so", - test_file("core/test_dlfcn_jspi_side.c"), - "-sSIDE_MODULE", - ] + self.get_emcc_args() + '-o', + 'side_b.so', + test_file('core/test_dlfcn_jspi_side_b.c'), + '-sSIDE_MODULE', + ] + + self.get_emcc_args() + ) + self.run_process( + [ + EMCC, + '-o', + 'side_a.so', + test_file('core/test_dlfcn_jspi_side_a.c'), + '-sSIDE_MODULE', + ] + + self.get_emcc_args() ) - self.do_run_in_out_file_test("core/test_dlfcn_jspi.c", emcc_args=["side.so", "-sMAIN_MODULE=2"]) + self.do_run_in_out_file_test('core/test_dlfcn_jspi.c', emcc_args=['side_a.so', 'side_b.so', '-sMAIN_MODULE=2', '-g']) @needs_dylink def test_dlfcn_rtld_local(self): From d787a87fd0bed3592fdd05b04e529329effbc22e Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Mon, 21 Apr 2025 17:38:42 -0400 Subject: [PATCH 2/2] Update test expectations --- test/core/test_dlfcn_jspi.c | 2 ++ test/core/test_dlfcn_jspi.out | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/test/core/test_dlfcn_jspi.c b/test/core/test_dlfcn_jspi.c index 9238a8f0bdd38..0458ddb5ba550 100644 --- a/test/core/test_dlfcn_jspi.c +++ b/test/core/test_dlfcn_jspi.c @@ -51,7 +51,9 @@ EM_JS(void, js_trampoline, (), { }) int main() { + printf("Suspending test\n"); helper(test_suspending_wrapper); + printf("Non suspending test\n"); js_trampoline(); printf("done\n"); return 0; diff --git a/test/core/test_dlfcn_jspi.out b/test/core/test_dlfcn_jspi.out index 2b184530b264d..6b7fe1dbd519a 100644 --- a/test/core/test_dlfcn_jspi.out +++ b/test/core/test_dlfcn_jspi.out @@ -1,5 +1,12 @@ +Suspending test side_module_trampoline_a side_module_trampoline_b sleeping slept -done 77 +okay 77 +Non suspending test +side_module_trampoline_a +side_module_trampoline_b +sync +okay 77 +done