Skip to content

Commit 55ff7e0

Browse files
committed
Fix late-binding symbols with JSPI
Late-binding symbols get a JS stub import that resolves the symbol and then makes an onward call. This breaks JSPI. (It also canonicalizes NaNs by round tripping them through JS). To fix this, we have to walk the import table and make wasm module stubs for each of the missing function-type symbols. These stubs will call a JS function to resolve the symbol but then insert it into a table and return control back to the wasm stub which has to make the onward call. This currently relies on --experimental-wasm-type-reflection. To avoid that, we need to parse the import table of the wasm binary manually. Also, without wasm-type-reflection, we have no way to handle the case where someone calls `loadWebAssemblyModule()` on a WebAssembly module instead of a Uint8Array.
1 parent a9651ff commit 55ff7e0

File tree

6 files changed

+236
-16
lines changed

6 files changed

+236
-16
lines changed

Diff for: src/lib/libdylink.js

+201-9
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,179 @@ var LibraryDylink = {
596596
}
597597
},
598598
#endif
599+
$getStubImportModule__postset: `
600+
var stubImportModuleCache = new Map();
601+
`,
602+
$getStubImportModule__deps: [
603+
"$generateFuncType",
604+
"$uleb128Encode"
605+
],
606+
// We need to call out to JS to resolve the function, but then make the actual
607+
// onward call from WebAssembly. The JavaScript $resolve function is
608+
// responsible for putting the appropriate function in the table. When the sig
609+
// is ii, the wat for the generated module looks like this:
610+
//
611+
// (module
612+
// (type $r (func ))
613+
// (type $ii (func (param i32) (result i32)))
614+
// (import "e" "t" (table 0 funcref))
615+
// (import "e" "r" (func $resolve))
616+
// (global $isResolved (mut i32) i32.const 0)
617+
//
618+
// (func (export "o") (param $p1 i32) (result i32)
619+
// global.get $isResolved
620+
// i32.eqz
621+
// if
622+
// call $resolve
623+
// i32.const 1
624+
// global.set $isResolved
625+
// end
626+
// local.get $p1
627+
// i32.const 0
628+
// call_indirect (type $ii)
629+
// )
630+
// )
631+
$getStubImportModule: (sig) => {
632+
var cached = stubImportModuleCache.get(sig);
633+
if (cached) {
634+
return cached;
635+
}
636+
const bytes = [
637+
0x00, 0x61, 0x73, 0x6d, // Magic number
638+
0x01, 0x00, 0x00, 0x00, // version 1
639+
0x01, // Type section code
640+
];
641+
var typeSectionBody = [
642+
0x02, // count: 2
643+
];
644+
generateFuncType('v', typeSectionBody);
645+
generateFuncType(sig, typeSectionBody);
646+
uleb128Encode(typeSectionBody.length, bytes);
647+
bytes.push(...typeSectionBody);
648+
649+
// static sections
650+
bytes.push(
651+
// Import section
652+
0x02, 0x0f,
653+
0x02, // 2 imports
654+
0x01, 0x65, // e
655+
0x01, 0x74, // t
656+
// funcref table with no limits
657+
0x01, 0x70, 0x00, 0x00,
658+
659+
0x01, 0x65, // e
660+
0x01, 0x72, // r
661+
0x00, 0x00, // function of type 0 ('v')
662+
663+
// Function section
664+
0x03, 0x02,
665+
0x01, 0x01, // One function of type 1 (sig)
666+
667+
// Globals section
668+
0x06, 0x06,
669+
0x01, // one global
670+
0x7f, 0x01, // i32 mut
671+
0x41, 0x00, 0x0b, // initialized to i32.const 0
672+
673+
// Export section
674+
0x07, 0x05,
675+
0x01, // one export
676+
0x01, 0x6f, // o
677+
0x00, 0x01, // function at index 1
678+
);
679+
bytes.push(0x0a); // Code section
680+
var codeBody = [
681+
0x00, // 0 locals
682+
0x23, 0x00, // global.get 0
683+
0x45, // i32.eqz
684+
0x04, // if
685+
0x40, 0x10, 0x00, // Call function 0
686+
0x41, 0x01, // i32.const 1
687+
0x24, 0x00, // global.set 0
688+
0x0b, // end
689+
];
690+
for (let i = 0; i < sig.length - 1; i++) {
691+
codeBody.push(0x20, i);
692+
}
599693

694+
codeBody.push(
695+
0x41, 0x00, // i32.const 0
696+
0x11, 0x01, 0x00, // call_indirect table 0, type 0
697+
0x0b // end
698+
);
699+
var codeSectionBody = [0x01];
700+
uleb128Encode(codeBody.length, codeSectionBody);
701+
codeSectionBody.push(...codeBody);
702+
uleb128Encode(codeSectionBody.length, bytes);
703+
bytes.push(...codeSectionBody);
704+
var result = new WebAssembly.Module(new Uint8Array(bytes));
705+
stubImportModuleCache.set(sig, result);
706+
return result;
707+
},
708+
709+
$wasmSigToEmscripten: (type) => {
710+
var lookup = {i32: 'i', i64: 'j', f32: 'f', f64: 'd', externref: 'e'};
711+
var sig = 'v';
712+
if (type.results.length) {
713+
sig = lookup[type.results[0]];
714+
}
715+
for (var v of type.parameters) {
716+
sig += lookup[v];
717+
}
718+
return sig;
719+
},
720+
$getStubImportResolver: (name, sig, table, resolveSymbol) => {
721+
return function r() {
722+
var res = resolveSymbol(name);
723+
if (res.orig) {
724+
res = res.orig;
725+
}
726+
try {
727+
// Attempting to call this with JS function will cause table.set() to fail
728+
table.set(0, res);
729+
} catch (err) {
730+
if (!(err instanceof TypeError)) {
731+
throw err;
732+
}
733+
var wrapped = convertJsFunctionToWasm(res, sig);
734+
table.set(0, wrapped);
735+
}
736+
};
737+
},
738+
$addStubImports__deps: [
739+
"$getStubImportModule",
740+
"$wasmSigToEmscripten",
741+
"$getStubImportResolver"
742+
],
743+
$addStubImports: (mod, stubs, resolveSymbol) => {
744+
// Assumes --experimental-wasm-type-reflection to get type field of WebAssembly.Module.imports().
745+
// TODO: Make this work without it.
746+
for (const {module, name, kind, type} of WebAssembly.Module.imports(mod)) {
747+
if (module !== 'env') {
748+
continue;
749+
}
750+
if (kind !== 'function') {
751+
continue;
752+
}
753+
if (name in wasmImports && !wasmImports[name].stub) {
754+
continue;
755+
}
756+
#if !DISABLE_EXCEPTION_CATCHING || SUPPORT_LONGJMP == 'emscripten'
757+
if (name.startsWith("invoke_")) {
758+
// JSPI + JS exceptions probably doesn't work but maybe nobody will
759+
// attempt stack switch inside a try block.
760+
stubs[name] = createInvokeFunction(name.split('_')[1]);
761+
continue;
762+
}
763+
#endif
764+
var sig = wasmSigToEmscripten(type);
765+
var mod = getStubImportModule(sig);
766+
const t = new WebAssembly.Table({element: 'funcref', initial: 1});
767+
const r = getStubImportResolver(name, sig, t, resolveSymbol);
768+
const inst = new WebAssembly.Instance(mod, {e: {t, r}});
769+
stubs[name] = inst.exports.o;
770+
}
771+
},
600772
// Loads a side module from binary data or compiled Module. Returns the module's exports or a
601773
// promise that resolves to its exports if the loadAsync flag is set.
602774
$loadWebAssemblyModule__docs: `
@@ -613,6 +785,9 @@ var LibraryDylink = {
613785
'$updateTableMap',
614786
'$wasmTable',
615787
'$addOnPostCtor',
788+
#if JSPI
789+
'$addStubImports',
790+
#endif
616791
],
617792
$loadWebAssemblyModule: (binary, flags, libName, localScope, handle) => {
618793
#if DYLINK_DEBUG
@@ -697,6 +872,7 @@ var LibraryDylink = {
697872
// to add the indirection for, without inspecting what A's imports
698873
// are. To do that here, we use a JS proxy (another option would
699874
// be to inspect the binary directly).
875+
const stubs = {};
700876
var proxyHandler = {
701877
get(stubs, prop) {
702878
// symbols that should be local to this module
@@ -728,16 +904,20 @@ var LibraryDylink = {
728904
// Return a stub function that will resolve the symbol
729905
// when first called.
730906
if (!(prop in stubs)) {
907+
#if JSPI
908+
throw new Error("Missing stub for " + prop);
909+
#else
731910
var resolved;
732911
stubs[prop] = (...args) => {
733912
resolved ||= resolveSymbol(prop);
734913
return resolved(...args);
735914
};
915+
#endif
736916
}
737917
return stubs[prop];
738918
}
739919
};
740-
var proxy = new Proxy({}, proxyHandler);
920+
var proxy = new Proxy(stubs, proxyHandler);
741921
var info = {
742922
'GOT.mem': new Proxy({}, GOTHandler),
743923
'GOT.func': new Proxy({}, GOTHandler),
@@ -877,16 +1057,28 @@ var LibraryDylink = {
8771057
}
8781058

8791059
if (flags.loadAsync) {
880-
if (binary instanceof WebAssembly.Module) {
881-
var instance = new WebAssembly.Instance(binary, info);
882-
return Promise.resolve(postInstantiation(binary, instance));
883-
}
884-
return WebAssembly.instantiate(binary, info).then(
885-
(result) => postInstantiation(result.module, result.instance)
886-
);
1060+
return (async function() {
1061+
if (binary instanceof WebAssembly.Module) {
1062+
#if JSPI
1063+
addStubImports(binary, stubs, resolveSymbol);
1064+
#endif
1065+
var instance = new WebAssembly.Instance(binary, info);
1066+
return postInstantiation(binary, instance);
1067+
}
1068+
#if JSPI
1069+
var module = await WebAssembly.compile(binary);
1070+
addStubImports(module, stubs, resolveSymbol);
1071+
var instance = await WebAssembly.instantiate(module, info);
1072+
#else
1073+
var {module, instance} = await WebAssembly.instantiate(binary, info);
1074+
#endif
1075+
return postInstantiation(module, instance);
1076+
})();
8871077
}
888-
8891078
var module = binary instanceof WebAssembly.Module ? binary : new WebAssembly.Module(binary);
1079+
#if JSPI
1080+
addStubImports(module, stubs, resolveSymbol);
1081+
#endif
8901082
var instance = new WebAssembly.Instance(module, info);
8911083
return postInstantiation(module, instance);
8921084
}

Diff for: test/core/test_dlfcn_jspi.c

+6-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// University of Illinois/NCSA Open Source License. Both these licenses can be
44
// found in the LICENSE file.
55

6+
#include <assert.h>
67
#include <emscripten.h>
78
#include <stdio.h>
89
#include <dlfcn.h>
@@ -21,9 +22,11 @@ int test_wrapper() {
2122
typedef int (*F)();
2223

2324
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();
25+
void* handle = dlopen("side_a.so", RTLD_NOW|RTLD_GLOBAL);
26+
assert(handle != NULL);
27+
F side_module_trampolinea = dlsym(handle, "side_module_trampoline_a");
28+
assert(side_module_trampolinea != NULL);
29+
int res = side_module_trampolinea();
2730
printf("done %d\n", res);
2831
return 0;
2932
}

Diff for: test/core/test_dlfcn_jspi.out

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
side_module_trampoline
1+
side_module_trampoline_a
2+
side_module_trampoline_b
23
sleeping
34
slept
45
done 77

Diff for: test/core/test_dlfcn_jspi_side_a.c

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include <stdio.h>
2+
int side_module_trampoline_b(void);
3+
4+
int side_module_trampoline_a() {
5+
printf("side_module_trampoline_a\n");
6+
return side_module_trampoline_b();
7+
}

Diff for: test/core/test_dlfcn_jspi_side_b.c

+7
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_b() {
5+
printf("side_module_trampoline_b\n");
6+
return test_wrapper();
7+
}

Diff for: test/test_core.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -3769,13 +3769,23 @@ def test_dlfcn_jspi(self):
37693769
[
37703770
EMCC,
37713771
"-o",
3772-
"side.so",
3773-
test_file("core/test_dlfcn_jspi_side.c"),
3772+
"side_a.so",
3773+
test_file("core/test_dlfcn_jspi_side_a.c"),
37743774
"-sSIDE_MODULE",
37753775
]
37763776
+ self.get_emcc_args()
37773777
)
3778-
self.do_run_in_out_file_test("core/test_dlfcn_jspi.c", emcc_args=["side.so", "-sMAIN_MODULE=2"])
3778+
self.run_process(
3779+
[
3780+
EMCC,
3781+
"-o",
3782+
"side_b.so",
3783+
test_file("core/test_dlfcn_jspi_side_b.c"),
3784+
"-sSIDE_MODULE",
3785+
]
3786+
+ self.get_emcc_args()
3787+
)
3788+
self.do_run_in_out_file_test("core/test_dlfcn_jspi.c", emcc_args=["side_a.so", "side_b.so", "-sMAIN_MODULE=2"])
37793789

37803790
@needs_dylink
37813791
def test_dlfcn_rtld_local(self):

0 commit comments

Comments
 (0)