Skip to content

Commit 43fdb70

Browse files
authored
gh-145037: Fix Emscripten trampoline with emcc >= 4.0.19 (#145038)
This undoes a change made as a part of PR 137470, for compatibility with EMSDK 4.0.19. It adds `emscripten_trampoline` field in `pycore_runtime_structs.h` and initializes it from JS initialization code with the wasm-gc based trampoline if possible. Otherwise we fall back to the JS trampoline.
1 parent 7eb00ad commit 43fdb70

File tree

5 files changed

+90
-24
lines changed

5 files changed

+90
-24
lines changed

Include/internal/pycore_emscripten_trampoline.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@
2727

2828
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
2929

30-
void
31-
_Py_EmscriptenTrampoline_Init(_PyRuntimeState *runtime);
32-
3330
PyObject*
3431
_PyEM_TrampolineCall(PyCFunctionWithKeywords func,
3532
PyObject* self,

Include/internal/pycore_runtime_structs.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,16 @@ struct pyruntimestate {
275275
struct _types_runtime_state types;
276276
struct _Py_time_runtime_state time;
277277

278+
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
279+
// Used in "Python/emscripten_trampoline.c" to choose between wasm-gc
280+
// trampoline and JavaScript trampoline.
281+
PyObject* (*emscripten_trampoline)(int* success,
282+
PyCFunctionWithKeywords func,
283+
PyObject* self,
284+
PyObject* args,
285+
PyObject* kw);
286+
#endif
287+
278288
/* All the objects that are shared by the runtime's interpreters. */
279289
struct _Py_cached_objects cached_objects;
280290
struct _Py_static_objects static_objects;

Python/emscripten_trampoline.c

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,59 @@
22

33
#include <emscripten.h> // EM_JS, EM_JS_DEPS
44
#include <Python.h>
5+
#include "pycore_runtime.h" // _PyRuntime
56

6-
EM_JS(
7-
PyObject*,
8-
_PyEM_TrampolineCall_inner, (int* success,
9-
PyCFunctionWithKeywords func,
10-
PyObject *arg1,
11-
PyObject *arg2,
12-
PyObject *arg3), {
13-
// JavaScript fallback trampoline
7+
// We use the _PyRuntime.emscripten_trampoline field to store a function pointer
8+
// for a wasm-gc based trampoline if it works. Otherwise fall back to JS
9+
// trampoline. The JS trampoline breaks stack switching but every runtime that
10+
// supports stack switching also supports wasm-gc.
11+
//
12+
// We'd like to make the trampoline call into a direct call but currently we
13+
// need to import the wasmTable to compile trampolineModule. emcc >= 4.0.19
14+
// defines the table in WebAssembly and exports it so we won't have access to it
15+
// until after the main module is compiled.
16+
//
17+
// To fix this, one natural solution would be to pass a funcref to the
18+
// trampoline instead of a table index. Several PRs would be needed to fix
19+
// things in llvm and emscripten in order to make this possible.
20+
//
21+
// The performance costs of an extra call_indirect aren't that large anyways.
22+
// The JIT should notice that the target is always the same and turn into a
23+
// check
24+
//
25+
// if (call_target != expected) deoptimize;
26+
// direct_call(call_target, args);
27+
28+
// Offset of emscripten_trampoline in _PyRuntimeState. There's a couple of
29+
// alternatives:
30+
//
31+
// 1. Just make emscripten_trampoline a real C global variable instead of a
32+
// field of _PyRuntimeState. This would violate our rule against mutable
33+
// globals.
34+
//
35+
// 2. #define a preprocessor constant equal to a hard coded number and make a
36+
// _Static_assert(offsetof(_PyRuntimeState, emscripten_trampoline) == OURCONSTANT)
37+
// This has the disadvantage that we have to update the hard coded constant
38+
// when _PyRuntimeState changes
39+
//
40+
// So putting the mutable constant in _PyRuntime and using a immutable global to
41+
// record the offset so we can access it from JS is probably the best way.
42+
EMSCRIPTEN_KEEPALIVE const int _PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET = offsetof(_PyRuntimeState, emscripten_trampoline);
43+
44+
typedef PyObject* (*TrampolineFunc)(int* success,
45+
PyCFunctionWithKeywords func,
46+
PyObject* self,
47+
PyObject* args,
48+
PyObject* kw);
49+
50+
/**
51+
* Backwards compatible trampoline works with all JS runtimes
52+
*/
53+
EM_JS(PyObject*, _PyEM_TrampolineCall_JS, (PyCFunctionWithKeywords func, PyObject *arg1, PyObject *arg2, PyObject *arg3), {
1454
return wasmTable.get(func)(arg1, arg2, arg3);
1555
}
16-
// Try to replace the JS definition of _PyEM_TrampolineCall_inner with a wasm
17-
// version.
18-
(function () {
56+
// Try to compile wasm-gc trampoline if possible.
57+
function getPyEMTrampolinePtr() {
1958
// Starting with iOS 18.3.1, WebKit on iOS has an issue with the garbage
2059
// collector that breaks the call trampoline. See #130418 and
2160
// https://bugs.webkit.org/show_bug.cgi?id=293113 for details.
@@ -27,19 +66,32 @@ _PyEM_TrampolineCall_inner, (int* success,
2766
(navigator.platform === 'MacIntel' && typeof navigator.maxTouchPoints !== 'undefined' && navigator.maxTouchPoints > 1)
2867
);
2968
if (isIOS) {
30-
return;
69+
return 0;
3170
}
71+
let trampolineModule;
3272
try {
33-
const trampolineModule = getWasmTrampolineModule();
34-
const trampolineInstance = new WebAssembly.Instance(trampolineModule, {
35-
env: { __indirect_function_table: wasmTable, memory: wasmMemory },
36-
});
37-
_PyEM_TrampolineCall_inner = trampolineInstance.exports.trampoline_call;
73+
trampolineModule = getWasmTrampolineModule();
3874
} catch (e) {
3975
// Compilation error due to missing wasm-gc support, fall back to JS
4076
// trampoline
77+
return 0;
4178
}
42-
})();
79+
const trampolineInstance = new WebAssembly.Instance(trampolineModule, {
80+
env: { __indirect_function_table: wasmTable, memory: wasmMemory },
81+
});
82+
return addFunction(trampolineInstance.exports.trampoline_call);
83+
}
84+
// We have to be careful to work correctly with memory snapshots -- the value of
85+
// _PyRuntimeState.emscripten_trampoline needs to reflect whether wasm-gc is
86+
// available in the current runtime, not in the runtime the snapshot was taken
87+
// in. This writes the appropriate value to
88+
// _PyRuntimeState.emscripten_trampoline from JS startup code that runs every
89+
// time, whether we are restoring a snapshot or not.
90+
addOnPreRun(function setEmscriptenTrampoline() {
91+
const ptr = getPyEMTrampolinePtr();
92+
const offset = HEAP32[__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET / 4];
93+
HEAP32[(__PyRuntime + offset) / 4] = ptr;
94+
});
4395
);
4496

4597
PyObject*
@@ -48,12 +100,19 @@ _PyEM_TrampolineCall(PyCFunctionWithKeywords func,
48100
PyObject* args,
49101
PyObject* kw)
50102
{
103+
TrampolineFunc trampoline = _PyRuntime.emscripten_trampoline;
104+
if (trampoline == 0) {
105+
return _PyEM_TrampolineCall_JS(func, self, args, kw);
106+
}
51107
int success = 1;
52-
PyObject *result = _PyEM_TrampolineCall_inner(&success, func, self, args, kw);
108+
PyObject *result = trampoline(&success, func, self, args, kw);
53109
if (!success) {
54110
PyErr_SetString(PyExc_SystemError, "Handler takes too many arguments");
55111
}
56112
return result;
57113
}
58114

115+
#else
116+
// This is exported so we need to define it even when it isn't used
117+
__attribute__((used)) const int _PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET = 0;
59118
#endif

configure

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

configure.ac

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2357,7 +2357,7 @@ AS_CASE([$ac_sys_system],
23572357
dnl Include file system support
23582358
AS_VAR_APPEND([LINKFORSHARED], [" -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"])
23592359
AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY"])
2360-
AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,_PyGILState_GetThisThreadState,__Py_DumpTraceback"])
2360+
AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,_PyGILState_GetThisThreadState,__Py_DumpTraceback,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET"])
23612361
AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"])
23622362
dnl Avoid bugs in JS fallback string decoding path
23632363
AS_VAR_APPEND([LINKFORSHARED], [" -sTEXTDECODER=2"])

0 commit comments

Comments
 (0)