Skip to content

Commit d0ecbdd

Browse files
authored
gh-128627: Emscripten: Use wasm-gc based call adaptor if available (#128628)
Replaces the trampoline mechanism in Emscripten with an implementation that uses a recently added feature of wasm-gc instead of JS type reflection, when that feature is available.
1 parent 5e65a1a commit d0ecbdd

File tree

4 files changed

+187
-71
lines changed

4 files changed

+187
-71
lines changed

Include/internal/pycore_emscripten_trampoline.h

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,14 @@
2727

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

30-
void _Py_EmscriptenTrampoline_Init(_PyRuntimeState *runtime);
30+
void
31+
_Py_EmscriptenTrampoline_Init(_PyRuntimeState *runtime);
3132

3233
PyObject*
33-
_PyEM_TrampolineCall_JavaScript(PyCFunctionWithKeywords func,
34-
PyObject* self,
35-
PyObject* args,
36-
PyObject* kw);
37-
38-
PyObject*
39-
_PyEM_TrampolineCall_Reflection(PyCFunctionWithKeywords func,
40-
PyObject* self,
41-
PyObject* args,
42-
PyObject* kw);
43-
44-
#define _PyEM_TrampolineCall(meth, self, args, kw) \
45-
((_PyRuntime.wasm_type_reflection_available) ? \
46-
(_PyEM_TrampolineCall_Reflection((PyCFunctionWithKeywords)(meth), (self), (args), (kw))) : \
47-
(_PyEM_TrampolineCall_JavaScript((PyCFunctionWithKeywords)(meth), (self), (args), (kw))))
34+
_PyEM_TrampolineCall(PyCFunctionWithKeywords func,
35+
PyObject* self,
36+
PyObject* args,
37+
PyObject* kw);
4838

4939
#define _PyCFunction_TrampolineCall(meth, self, args) \
5040
_PyEM_TrampolineCall( \
@@ -62,8 +52,6 @@ _PyEM_TrampolineCall_Reflection(PyCFunctionWithKeywords func,
6252

6353
#else // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
6454

65-
#define _Py_EmscriptenTrampoline_Init(runtime)
66-
6755
#define _PyCFunction_TrampolineCall(meth, self, args) \
6856
(meth)((self), (args))
6957

Include/internal/pycore_runtime.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ typedef struct pyruntimestate {
172172
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
173173
// Used in "Python/emscripten_trampoline.c" to choose between type
174174
// reflection trampoline and EM_JS trampoline.
175-
bool wasm_type_reflection_available;
175+
int (*emscripten_count_args_function)(PyCFunctionWithKeywords func);
176176
#endif
177177

178178
/* All the objects that are shared by the runtime's interpreters. */
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
For Emscripten builds the function pointer cast call trampoline now uses the
2+
wasm-gc ref.test instruction if it's available instead of Wasm JS type
3+
reflection.

Python/emscripten_trampoline.c

Lines changed: 177 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,205 @@
11
#if defined(PY_CALL_TRAMPOLINE)
22

3-
#include <emscripten.h> // EM_JS
3+
#include <emscripten.h> // EM_JS, EM_JS_DEPS
44
#include <Python.h>
55
#include "pycore_runtime.h" // _PyRuntime
66

7+
typedef int (*CountArgsFunc)(PyCFunctionWithKeywords func);
78

8-
/**
9-
* This is the GoogleChromeLabs approved way to feature detect type-reflection:
10-
* https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/type-reflection/index.js
11-
*/
12-
EM_JS(int, _PyEM_detect_type_reflection, (), {
13-
if (!("Function" in WebAssembly)) {
14-
return false;
15-
}
16-
if (WebAssembly.Function.type) {
17-
// Node v20
18-
Module.PyEM_CountArgs = (func) => WebAssembly.Function.type(wasmTable.get(func)).parameters.length;
19-
} else {
20-
// Node >= 22, v8-based browsers
21-
Module.PyEM_CountArgs = (func) => wasmTable.get(func).type().parameters.length;
9+
// Offset of emscripten_count_args_function in _PyRuntimeState. There's a couple
10+
// of alternatives:
11+
// 1. Just make emscripten_count_args_function a real C global variable instead
12+
// of a field of _PyRuntimeState. This would violate our rule against mutable
13+
// globals.
14+
// 2. #define a preprocessor constant equal to a hard coded number and make a
15+
// _Static_assert(offsetof(_PyRuntimeState, emscripten_count_args_function)
16+
// == OURCONSTANT) This has the disadvantage that we have to update the hard
17+
// coded constant when _PyRuntimeState changes
18+
//
19+
// So putting the mutable constant in _PyRuntime and using a immutable global to
20+
// record the offset so we can access it from JS is probably the best way.
21+
EMSCRIPTEN_KEEPALIVE const int _PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET = offsetof(_PyRuntimeState, emscripten_count_args_function);
22+
23+
EM_JS(CountArgsFunc, _PyEM_GetCountArgsPtr, (), {
24+
return Module._PyEM_CountArgsPtr; // initialized below
25+
}
26+
// Binary module for the checks. It has to be done in web assembly because
27+
// clang/llvm have no support yet for the reference types yet. In fact, the wasm
28+
// binary toolkit doesn't yet support the ref.test instruction either. To
29+
// convert the following module to the binary, my approach is to find and
30+
// replace "ref.test $type" -> "drop i32.const n" on the source text. This
31+
// results in the bytes "0x1a, 0x41, n" where we need the bytes "0xfb, 0x14, n"
32+
// so doing a find and replace on the output from "0x1a, 0x41" -> "0xfb, 0x14"
33+
// gets us the output we need.
34+
//
35+
// (module
36+
// (type $type0 (func (param) (result i32)))
37+
// (type $type1 (func (param i32) (result i32)))
38+
// (type $type2 (func (param i32 i32) (result i32)))
39+
// (type $type3 (func (param i32 i32 i32) (result i32)))
40+
// (type $blocktype (func (param i32) (result)))
41+
// (table $funcs (import "e" "t") 0 funcref)
42+
// (export "f" (func $f))
43+
// (func $f (param $fptr i32) (result i32)
44+
// (local $fref funcref)
45+
// local.get $fptr
46+
// table.get $funcs
47+
// local.tee $fref
48+
// ref.test $type3
49+
// (block $b (type $blocktype)
50+
// i32.eqz
51+
// br_if $b
52+
// i32.const 3
53+
// return
54+
// )
55+
// local.get $fref
56+
// ref.test $type2
57+
// (block $b (type $blocktype)
58+
// i32.eqz
59+
// br_if $b
60+
// i32.const 2
61+
// return
62+
// )
63+
// local.get $fref
64+
// ref.test $type1
65+
// (block $b (type $blocktype)
66+
// i32.eqz
67+
// br_if $b
68+
// i32.const 1
69+
// return
70+
// )
71+
// local.get $fref
72+
// ref.test $type0
73+
// (block $b (type $blocktype)
74+
// i32.eqz
75+
// br_if $b
76+
// i32.const 0
77+
// return
78+
// )
79+
// i32.const -1
80+
// )
81+
// )
82+
addOnPreRun(() => {
83+
// Try to initialize countArgsFunc
84+
const code = new Uint8Array([
85+
0x00, 0x61, 0x73, 0x6d, // \0asm magic number
86+
0x01, 0x00, 0x00, 0x00, // version 1
87+
0x01, 0x1b, // Type section, body is 0x1b bytes
88+
0x05, // 6 entries
89+
0x60, 0x00, 0x01, 0x7f, // (type $type0 (func (param) (result i32)))
90+
0x60, 0x01, 0x7f, 0x01, 0x7f, // (type $type1 (func (param i32) (result i32)))
91+
0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, // (type $type2 (func (param i32 i32) (result i32)))
92+
0x60, 0x03, 0x7f, 0x7f, 0x7f, 0x01, 0x7f, // (type $type3 (func (param i32 i32 i32) (result i32)))
93+
0x60, 0x01, 0x7f, 0x00, // (type $blocktype (func (param i32) (result)))
94+
0x02, 0x09, // Import section, 0x9 byte body
95+
0x01, // 1 import (table $funcs (import "e" "t") 0 funcref)
96+
0x01, 0x65, // "e"
97+
0x01, 0x74, // "t"
98+
0x01, // importing a table
99+
0x70, // of entry type funcref
100+
0x00, 0x00, // table limits: no max, min of 0
101+
0x03, 0x02, // Function section
102+
0x01, 0x01, // We're going to define one function of type 1 (func (param i32) (result i32))
103+
0x07, 0x05, // export section
104+
0x01, // 1 export
105+
0x01, 0x66, // called "f"
106+
0x00, // a function
107+
0x00, // at index 0
108+
109+
0x0a, 0x44, // Code section,
110+
0x01, 0x42, // one entry of length 50
111+
0x01, 0x01, 0x70, // one local of type funcref
112+
// Body of the function
113+
0x20, 0x00, // local.get $fptr
114+
0x25, 0x00, // table.get $funcs
115+
0x22, 0x01, // local.tee $fref
116+
0xfb, 0x14, 0x03, // ref.test $type3
117+
0x02, 0x04, // block $b (type $blocktype)
118+
0x45, // i32.eqz
119+
0x0d, 0x00, // br_if $b
120+
0x41, 0x03, // i32.const 3
121+
0x0f, // return
122+
0x0b, // end block
123+
124+
0x20, 0x01, // local.get $fref
125+
0xfb, 0x14, 0x02, // ref.test $type2
126+
0x02, 0x04, // block $b (type $blocktype)
127+
0x45, // i32.eqz
128+
0x0d, 0x00, // br_if $b
129+
0x41, 0x02, // i32.const 2
130+
0x0f, // return
131+
0x0b, // end block
132+
133+
0x20, 0x01, // local.get $fref
134+
0xfb, 0x14, 0x01, // ref.test $type1
135+
0x02, 0x04, // block $b (type $blocktype)
136+
0x45, // i32.eqz
137+
0x0d, 0x00, // br_if $b
138+
0x41, 0x01, // i32.const 1
139+
0x0f, // return
140+
0x0b, // end block
141+
142+
0x20, 0x01, // local.get $fref
143+
0xfb, 0x14, 0x00, // ref.test $type0
144+
0x02, 0x04, // block $b (type $blocktype)
145+
0x45, // i32.eqz
146+
0x0d, 0x00, // br_if $b
147+
0x41, 0x00, // i32.const 0
148+
0x0f, // return
149+
0x0b, // end block
150+
151+
0x41, 0x7f, // i32.const -1
152+
0x0b // end function
153+
]);
154+
let ptr = 0;
155+
try {
156+
const mod = new WebAssembly.Module(code);
157+
const inst = new WebAssembly.Instance(mod, {e: {t: wasmTable}});
158+
ptr = addFunction(inst.exports.f);
159+
} catch(e) {
160+
// If something goes wrong, we'll null out _PyEM_CountFuncParams and fall
161+
// back to the JS trampoline.
22162
}
23-
return true;
163+
Module._PyEM_CountArgsPtr = ptr;
164+
const offset = HEAP32[__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET/4];
165+
HEAP32[__PyRuntime/4 + offset] = ptr;
24166
});
167+
);
25168

26169
void
27170
_Py_EmscriptenTrampoline_Init(_PyRuntimeState *runtime)
28171
{
29-
runtime->wasm_type_reflection_available = _PyEM_detect_type_reflection();
172+
runtime->emscripten_count_args_function = _PyEM_GetCountArgsPtr();
30173
}
31174

175+
// We have to be careful to work correctly with memory snapshots. Even if we are
176+
// loading a memory snapshot, we need to perform the JS initialization work.
177+
// That means we can't call the initialization code from C. Instead, we export
178+
// this function pointer to JS and then fill it in a preRun function which runs
179+
// unconditionally.
32180
/**
33181
* Backwards compatible trampoline works with all JS runtimes
34182
*/
35-
EM_JS(PyObject*,
36-
_PyEM_TrampolineCall_JavaScript, (PyCFunctionWithKeywords func,
37-
PyObject *arg1,
38-
PyObject *arg2,
39-
PyObject *arg3),
40-
{
183+
EM_JS(PyObject*, _PyEM_TrampolineCall_JS, (PyCFunctionWithKeywords func, PyObject *arg1, PyObject *arg2, PyObject *arg3), {
41184
return wasmTable.get(func)(arg1, arg2, arg3);
42-
}
43-
);
44-
45-
/**
46-
* In runtimes with WebAssembly type reflection, count the number of parameters
47-
* and cast to the appropriate signature
48-
*/
49-
EM_JS(int, _PyEM_CountFuncParams, (PyCFunctionWithKeywords func),
50-
{
51-
let n = _PyEM_CountFuncParams.cache.get(func);
52-
53-
if (n !== undefined) {
54-
return n;
55-
}
56-
n = Module.PyEM_CountArgs(func);
57-
_PyEM_CountFuncParams.cache.set(func, n);
58-
return n;
59-
}
60-
_PyEM_CountFuncParams.cache = new Map();
61-
)
62-
185+
});
63186

64187
typedef PyObject* (*zero_arg)(void);
65188
typedef PyObject* (*one_arg)(PyObject*);
66189
typedef PyObject* (*two_arg)(PyObject*, PyObject*);
67190
typedef PyObject* (*three_arg)(PyObject*, PyObject*, PyObject*);
68191

69-
70192
PyObject*
71-
_PyEM_TrampolineCall_Reflection(PyCFunctionWithKeywords func,
72-
PyObject* self,
73-
PyObject* args,
74-
PyObject* kw)
193+
_PyEM_TrampolineCall(PyCFunctionWithKeywords func,
194+
PyObject* self,
195+
PyObject* args,
196+
PyObject* kw)
75197
{
76-
switch (_PyEM_CountFuncParams(func)) {
198+
CountArgsFunc count_args = _PyRuntime.emscripten_count_args_function;
199+
if (count_args == 0) {
200+
return _PyEM_TrampolineCall_JS(func, self, args, kw);
201+
}
202+
switch (count_args(func)) {
77203
case 0:
78204
return ((zero_arg)func)();
79205
case 1:
@@ -83,8 +209,7 @@ _PyEM_TrampolineCall_Reflection(PyCFunctionWithKeywords func,
83209
case 3:
84210
return ((three_arg)func)(self, args, kw);
85211
default:
86-
PyErr_SetString(PyExc_SystemError,
87-
"Handler takes too many arguments");
212+
PyErr_SetString(PyExc_SystemError, "Handler takes too many arguments");
88213
return NULL;
89214
}
90215
}

0 commit comments

Comments
 (0)