Skip to content

Commit 0e4e608

Browse files
[3.14] gh-128627: Use __builtin_wasm_test_function_pointer_signature for Emscripten trampoline (GH-137470) (#139039)
gh-128627: Use __builtin_wasm_test_function_pointer_signature for Emscripten trampoline (GH-137470) With llvm/llvm-project#150201 being merged, there is now a better way to generate the Emscripten trampoline, instead of including hand-generated binary WASM content. Requires Emscripten 4.0.12. (cherry picked from commit 2629ee4) Co-authored-by: Hood Chatham <[email protected]>
1 parent 7a2854e commit 0e4e608

File tree

9 files changed

+129
-201
lines changed

9 files changed

+129
-201
lines changed

Include/internal/pycore_runtime_structs.h

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,6 @@ struct pyruntimestate {
279279
struct _types_runtime_state types;
280280
struct _Py_time_runtime_state time;
281281

282-
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
283-
// Used in "Python/emscripten_trampoline.c" to choose between type
284-
// reflection trampoline and EM_JS trampoline.
285-
int (*emscripten_count_args_function)(PyCFunctionWithKeywords func);
286-
#endif
287-
288282
/* All the objects that are shared by the runtime's interpreters. */
289283
struct _Py_cached_objects cached_objects;
290284
struct _Py_static_objects static_objects;

Makefile.pre.in

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3101,6 +3101,12 @@ config.status: $(srcdir)/configure
31013101
Python/asm_trampoline.o: $(srcdir)/Python/asm_trampoline.S
31023102
$(CC) -c $(PY_CORE_CFLAGS) -o $@ $<
31033103

3104+
Python/emscripten_trampoline_inner.wasm: $(srcdir)/Python/emscripten_trampoline_inner.c
3105+
# emcc has a path that ends with emsdk/upstream/emscripten/emcc, we're looking for emsdk/upstream/bin/clang.
3106+
$$(dirname $$(dirname $(CC)))/bin/clang -o $@ $< -mgc -O2 -Wl,--no-entry -Wl,--import-table -Wl,--import-memory -target wasm32-unknown-unknown -nostdlib
3107+
3108+
Python/emscripten_trampoline_wasm.c: Python/emscripten_trampoline_inner.wasm
3109+
$(PYTHON_FOR_REGEN) $(srcdir)/Tools/wasm/emscripten/prepare_external_wasm.py $< $@ getWasmTrampolineModule
31043110

31053111
JIT_DEPS = \
31063112
$(srcdir)/Tools/jit/*.c \

Python/emscripten_trampoline.c

Lines changed: 26 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -2,75 +2,20 @@
22

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

7-
typedef int (*CountArgsFunc)(PyCFunctionWithKeywords func);
8-
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
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
14+
return wasmTable.get(func)(arg1, arg2, arg3);
2515
}
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 textual wasm to a binary, you can build wabt from this
30-
// branch: https://github.com/WebAssembly/wabt/pull/2529 and then use that
31-
// wat2wasm binary.
32-
//
33-
// (module
34-
// (type $type0 (func (param) (result i32)))
35-
// (type $type1 (func (param i32) (result i32)))
36-
// (type $type2 (func (param i32 i32) (result i32)))
37-
// (type $type3 (func (param i32 i32 i32) (result i32)))
38-
// (type $blocktype (func (param) (result)))
39-
// (table $funcs (import "e" "t") 0 funcref)
40-
// (export "f" (func $f))
41-
// (func $f (param $fptr i32) (result i32)
42-
// (local $fref funcref)
43-
// local.get $fptr
44-
// table.get $funcs
45-
// local.tee $fref
46-
// ref.test $type3
47-
// if $blocktype
48-
// i32.const 3
49-
// return
50-
// end
51-
// local.get $fref
52-
// ref.test $type2
53-
// if $blocktype
54-
// i32.const 2
55-
// return
56-
// end
57-
// local.get $fref
58-
// ref.test $type1
59-
// if $blocktype
60-
// i32.const 1
61-
// return
62-
// end
63-
// local.get $fref
64-
// ref.test $type0
65-
// if $blocktype
66-
// i32.const 0
67-
// return
68-
// end
69-
// i32.const -1
70-
// )
71-
// )
72-
73-
function getPyEMCountArgsPtr() {
16+
// Try to replace the JS definition of _PyEM_TrampolineCall_inner with a wasm
17+
// version.
18+
(function () {
7419
// Starting with iOS 18.3.1, WebKit on iOS has an issue with the garbage
7520
// collector that breaks the call trampoline. See #130418 and
7621
// https://bugs.webkit.org/show_bug.cgi?id=293113 for details.
@@ -82,137 +27,33 @@ function getPyEMCountArgsPtr() {
8227
(navigator.platform === 'MacIntel' && typeof navigator.maxTouchPoints !== 'undefined' && navigator.maxTouchPoints > 1)
8328
);
8429
if (isIOS) {
85-
return 0;
30+
return;
8631
}
87-
88-
// Try to initialize countArgsFunc
89-
const code = new Uint8Array([
90-
0x00, 0x61, 0x73, 0x6d, // \0asm magic number
91-
0x01, 0x00, 0x00, 0x00, // version 1
92-
0x01, 0x1a, // Type section, body is 0x1a bytes
93-
0x05, // 6 entries
94-
0x60, 0x00, 0x01, 0x7f, // (type $type0 (func (param) (result i32)))
95-
0x60, 0x01, 0x7f, 0x01, 0x7f, // (type $type1 (func (param i32) (result i32)))
96-
0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, // (type $type2 (func (param i32 i32) (result i32)))
97-
0x60, 0x03, 0x7f, 0x7f, 0x7f, 0x01, 0x7f, // (type $type3 (func (param i32 i32 i32) (result i32)))
98-
0x60, 0x00, 0x00, // (type $blocktype (func (param) (result)))
99-
0x02, 0x09, // Import section, 0x9 byte body
100-
0x01, // 1 import (table $funcs (import "e" "t") 0 funcref)
101-
0x01, 0x65, // "e"
102-
0x01, 0x74, // "t"
103-
0x01, // importing a table
104-
0x70, // of entry type funcref
105-
0x00, 0x00, // table limits: no max, min of 0
106-
0x03, 0x02, // Function section
107-
0x01, 0x01, // We're going to define one function of type 1 (func (param i32) (result i32))
108-
0x07, 0x05, // export section
109-
0x01, // 1 export
110-
0x01, 0x66, // called "f"
111-
0x00, // a function
112-
0x00, // at index 0
113-
114-
0x0a, 56, // Code section,
115-
0x01, 54, // one entry of length 54
116-
0x01, 0x01, 0x70, // one local of type funcref
117-
// Body of the function
118-
0x20, 0x00, // local.get $fptr
119-
0x25, 0x00, // table.get $funcs
120-
0x22, 0x01, // local.tee $fref
121-
0xfb, 0x14, 0x03, // ref.test $type3
122-
0x04, 0x04, // if (type $blocktype)
123-
0x41, 0x03, // i32.const 3
124-
0x0f, // return
125-
0x0b, // end block
126-
127-
0x20, 0x01, // local.get $fref
128-
0xfb, 0x14, 0x02, // ref.test $type2
129-
0x04, 0x04, // if (type $blocktype)
130-
0x41, 0x02, // i32.const 2
131-
0x0f, // return
132-
0x0b, // end block
133-
134-
0x20, 0x01, // local.get $fref
135-
0xfb, 0x14, 0x01, // ref.test $type1
136-
0x04, 0x04, // if (type $blocktype)
137-
0x41, 0x01, // i32.const 1
138-
0x0f, // return
139-
0x0b, // end block
140-
141-
0x20, 0x01, // local.get $fref
142-
0xfb, 0x14, 0x00, // ref.test $type0
143-
0x04, 0x04, // if (type $blocktype)
144-
0x41, 0x00, // i32.const 0
145-
0x0f, // return
146-
0x0b, // end block
147-
148-
0x41, 0x7f, // i32.const -1
149-
0x0b // end function
150-
]);
15132
try {
152-
const mod = new WebAssembly.Module(code);
153-
const inst = new WebAssembly.Instance(mod, { e: { t: wasmTable } });
154-
return addFunction(inst.exports.f);
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;
15538
} catch (e) {
156-
// If something goes wrong, we'll null out _PyEM_CountFuncParams and fall
157-
// back to the JS trampoline.
158-
return 0;
39+
// Compilation error due to missing wasm-gc support, fall back to JS
40+
// trampoline
15941
}
160-
}
161-
162-
addOnPreRun(() => {
163-
const ptr = getPyEMCountArgsPtr();
164-
Module._PyEM_CountArgsPtr = ptr;
165-
const offset = HEAP32[__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET / 4];
166-
HEAP32[(__PyRuntime + offset) / 4] = ptr;
167-
});
42+
})();
16843
);
16944

170-
void
171-
_Py_EmscriptenTrampoline_Init(_PyRuntimeState *runtime)
172-
{
173-
runtime->emscripten_count_args_function = _PyEM_GetCountArgsPtr();
174-
}
175-
176-
// We have to be careful to work correctly with memory snapshots. Even if we are
177-
// loading a memory snapshot, we need to perform the JS initialization work.
178-
// That means we can't call the initialization code from C. Instead, we export
179-
// this function pointer to JS and then fill it in a preRun function which runs
180-
// unconditionally.
181-
/**
182-
* Backwards compatible trampoline works with all JS runtimes
183-
*/
184-
EM_JS(PyObject*, _PyEM_TrampolineCall_JS, (PyCFunctionWithKeywords func, PyObject *arg1, PyObject *arg2, PyObject *arg3), {
185-
return wasmTable.get(func)(arg1, arg2, arg3);
186-
});
187-
188-
typedef PyObject* (*zero_arg)(void);
189-
typedef PyObject* (*one_arg)(PyObject*);
190-
typedef PyObject* (*two_arg)(PyObject*, PyObject*);
191-
typedef PyObject* (*three_arg)(PyObject*, PyObject*, PyObject*);
192-
19345
PyObject*
19446
_PyEM_TrampolineCall(PyCFunctionWithKeywords func,
19547
PyObject* self,
19648
PyObject* args,
19749
PyObject* kw)
19850
{
199-
CountArgsFunc count_args = _PyRuntime.emscripten_count_args_function;
200-
if (count_args == 0) {
201-
return _PyEM_TrampolineCall_JS(func, self, args, kw);
202-
}
203-
switch (count_args(func)) {
204-
case 0:
205-
return ((zero_arg)func)();
206-
case 1:
207-
return ((one_arg)func)(self);
208-
case 2:
209-
return ((two_arg)func)(self, args);
210-
case 3:
211-
return ((three_arg)func)(self, args, kw);
212-
default:
213-
PyErr_SetString(PyExc_SystemError, "Handler takes too many arguments");
214-
return NULL;
51+
int success = 1;
52+
PyObject *result = _PyEM_TrampolineCall_inner(&success, func, self, args, kw);
53+
if (!success) {
54+
PyErr_SetString(PyExc_SystemError, "Handler takes too many arguments");
21555
}
56+
return result;
21657
}
21758

21859
#endif
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// This file must be compiled with -mgc to enable the extra wasm-gc
2+
// instructions. It has to be compiled separately because not enough JS runtimes
3+
// support wasm-gc yet. If the JS runtime does not support wasm-gc (or has buggy
4+
// support like iOS), we will use the JS trampoline fallback.
5+
6+
// We can't import Python.h here because it is compiled/linked with -nostdlib.
7+
// We don't need to know what's inside PyObject* anyways. We could just call it
8+
// void* everywhere. There are two reasons to do this:
9+
// 1. to improve readability
10+
// 2. eventually when we are comfortable requiring wasm-gc, we can merge this
11+
// into emscripten_trampoline.c without worrying about it.
12+
typedef void PyObject;
13+
14+
typedef PyObject* (*three_arg)(PyObject*, PyObject*, PyObject*);
15+
typedef PyObject* (*two_arg)(PyObject*, PyObject*);
16+
typedef PyObject* (*one_arg)(PyObject*);
17+
typedef PyObject* (*zero_arg)(void);
18+
19+
#define TRY_RETURN_CALL(ty, args...) \
20+
if (__builtin_wasm_test_function_pointer_signature((ty)func)) { \
21+
return ((ty)func)(args); \
22+
}
23+
24+
__attribute__((export_name("trampoline_call"))) PyObject*
25+
trampoline_call(int* success,
26+
void* func,
27+
PyObject* self,
28+
PyObject* args,
29+
PyObject* kw)
30+
{
31+
*success = 1;
32+
TRY_RETURN_CALL(three_arg, self, args, kw);
33+
TRY_RETURN_CALL(two_arg, self, args);
34+
TRY_RETURN_CALL(one_arg, self);
35+
TRY_RETURN_CALL(zero_arg);
36+
*success = 0;
37+
return 0;
38+
}

Python/pystate.c

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
#include "pycore_codecs.h" // _PyCodec_Fini()
99
#include "pycore_critical_section.h" // _PyCriticalSection_Resume()
1010
#include "pycore_dtoa.h" // _dtoa_state_INIT()
11-
#include "pycore_emscripten_trampoline.h" // _Py_EmscriptenTrampoline_Init()
1211
#include "pycore_freelist.h" // _PyObject_ClearFreeLists()
1312
#include "pycore_initconfig.h" // _PyStatus_OK()
1413
#include "pycore_interpframe.h" // _PyThreadState_HasStackSpace()
@@ -434,11 +433,6 @@ init_runtime(_PyRuntimeState *runtime,
434433
runtime->main_thread = PyThread_get_thread_ident();
435434

436435
runtime->unicode_state.ids.next_index = unicode_next_index;
437-
438-
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
439-
_Py_EmscriptenTrampoline_Init(runtime);
440-
#endif
441-
442436
runtime->_initialized = 1;
443437
}
444438

Tools/c-analyzer/cpython/_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def clean_lines(text):
6767
Python/dynload_hpux.c # dl.h
6868
Python/emscripten_signal.c
6969
Python/emscripten_syscalls.c
70+
Python/emscripten_trampoline_inner.c
7071
Python/thread_pthread.h
7172
Python/thread_pthread_stubs.h
7273
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import sys
5+
from pathlib import Path
6+
7+
JS_TEMPLATE = """
8+
#include "emscripten.h"
9+
10+
EM_JS(void, {function_name}, (void), {{
11+
return new WebAssembly.Module(hexStringToUTF8Array("{hex_string}"));
12+
}}
13+
function hexStringToUTF8Array(hex) {{
14+
const bytes = [];
15+
for (let i = 0; i < hex.length; i += 2) {{
16+
bytes.push(parseInt(hex.substr(i, 2), 16));
17+
}}
18+
return new Uint8Array(bytes);
19+
}});
20+
"""
21+
22+
def prepare_wasm(input_file, output_file, function_name):
23+
# Read the compiled WASM as binary and convert to hex
24+
wasm_bytes = Path(input_file).read_bytes()
25+
26+
hex_string = "".join(f"{byte:02x}" for byte in wasm_bytes)
27+
28+
# Generate JavaScript module
29+
js_content = JS_TEMPLATE.format(
30+
function_name=function_name, hex_string=hex_string
31+
)
32+
Path(output_file).write_text(js_content)
33+
34+
print(
35+
f"Successfully compiled {input_file} and generated {output_file}"
36+
)
37+
return 0
38+
39+
40+
def main():
41+
parser = argparse.ArgumentParser(
42+
description="Compile WebAssembly text files using wasm-as"
43+
)
44+
parser.add_argument("input_file", help="Input .wat file to compile")
45+
parser.add_argument("output_file", help="Output file name")
46+
parser.add_argument("function_name", help="Name of the export function")
47+
48+
args = parser.parse_args()
49+
50+
return prepare_wasm(args.input_file, args.output_file, args.function_name)
51+
52+
53+
if __name__ == "__main__":
54+
sys.exit(main())

0 commit comments

Comments
 (0)