Skip to content

Commit 6b179ad

Browse files
hoodmaneYhg1sbrettcannon
authored
gh-106213: Make Emscripten trampolines work with JSPI (GH-106219)
There is a WIP proposal to enable webassembly stack switching which have been implemented in v8: https://github.com/WebAssembly/js-promise-integration It is not possible to switch stacks that contain JS frames so the Emscripten JS trampolines that allow calling functions with the wrong number of arguments don't work in this case. However, the js-promise-integration proposal requires the [type reflection for Wasm/JS API](https://github.com/WebAssembly/js-types) proposal, which allows us to actually count the number of arguments a function expects. For better compatibility with stack switching, this PR checks if type reflection is available, and if so we use a switch block to decide the appropriate signature. If type reflection is unavailable, we should use the current EMJS trampoline. We cache the function argument counts since when I didn't cache them performance was negatively affected. Co-authored-by: T. Wouters <[email protected]> Co-authored-by: Brett Cannon <[email protected]>
1 parent 59073c9 commit 6b179ad

File tree

10 files changed

+183
-30
lines changed

10 files changed

+183
-30
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#ifndef Py_EMSCRIPTEN_TRAMPOLINE_H
2+
#define Py_EMSCRIPTEN_TRAMPOLINE_H
3+
4+
#include "pycore_runtime.h" // _PyRuntimeState
5+
6+
/**
7+
* C function call trampolines to mitigate bad function pointer casts.
8+
*
9+
* Section 6.3.2.3, paragraph 8 reads:
10+
*
11+
* A pointer to a function of one type may be converted to a pointer to a
12+
* function of another type and back again; the result shall compare equal to
13+
* the original pointer. If a converted pointer is used to call a function
14+
* whose type is not compatible with the pointed-to type, the behavior is
15+
* undefined.
16+
*
17+
* Typical native ABIs ignore additional arguments or fill in missing values
18+
* with 0/NULL in function pointer cast. Compilers do not show warnings when a
19+
* function pointer is explicitly casted to an incompatible type.
20+
*
21+
* Bad fpcasts are an issue in WebAssembly. WASM's indirect_call has strict
22+
* function signature checks. Argument count, types, and return type must match.
23+
*
24+
* Third party code unintentionally rely on problematic fpcasts. The call
25+
* trampoline mitigates common occurrences of bad fpcasts on Emscripten.
26+
*/
27+
28+
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
29+
30+
void _Py_EmscriptenTrampoline_Init(_PyRuntimeState *runtime);
31+
32+
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))))
48+
49+
#define _PyCFunction_TrampolineCall(meth, self, args) \
50+
_PyEM_TrampolineCall( \
51+
(*(PyCFunctionWithKeywords)(void(*)(void))(meth)), (self), (args), NULL)
52+
53+
#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \
54+
_PyEM_TrampolineCall((meth), (self), (args), (kw))
55+
56+
#define descr_set_trampoline_call(set, obj, value, closure) \
57+
((int)_PyEM_TrampolineCall((PyCFunctionWithKeywords)(set), (obj), (value), (PyObject*)(closure)))
58+
59+
#define descr_get_trampoline_call(get, obj, closure) \
60+
_PyEM_TrampolineCall((PyCFunctionWithKeywords)(get), (obj), (PyObject*)(closure), NULL)
61+
62+
63+
#else // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
64+
65+
#define _Py_EmscriptenTrampoline_Init(runtime)
66+
67+
#define _PyCFunction_TrampolineCall(meth, self, args) \
68+
(meth)((self), (args))
69+
70+
#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \
71+
(meth)((self), (args), (kw))
72+
73+
#define descr_set_trampoline_call(set, obj, value, closure) \
74+
(set)((obj), (value), (closure))
75+
76+
#define descr_get_trampoline_call(get, obj, closure) \
77+
(get)((obj), (closure))
78+
79+
#endif // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
80+
81+
#endif // ndef Py_EMSCRIPTEN_SIGNAL_H

Include/internal/pycore_object.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ extern "C" {
1010

1111
#include <stdbool.h>
1212
#include "pycore_gc.h" // _PyObject_GC_IS_TRACKED()
13+
#include "pycore_emscripten_trampoline.h" // _PyCFunction_TrampolineCall()
1314
#include "pycore_interp.h" // PyInterpreterState.gc
1415
#include "pycore_pystate.h" // _PyInterpreterState_GET()
1516

Include/internal/pycore_runtime.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,13 @@ typedef struct pyruntimestate {
267267

268268
/* PyInterpreterState.interpreters.main */
269269
PyInterpreterState _main_interpreter;
270+
271+
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
272+
// Used in "Python/emscripten_trampoline.c" to choose between type
273+
// reflection trampoline and EM_JS trampoline.
274+
bool wasm_type_reflection_available;
275+
#endif
276+
270277
} _PyRuntimeState;
271278

272279

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Changed the way that Emscripten call trampolines work for compatibility with
2+
Wasm/JS Promise integration.

Objects/descrobject.c

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "pycore_abstract.h" // _PyObject_RealIsSubclass()
55
#include "pycore_call.h" // _PyStack_AsDict()
66
#include "pycore_ceval.h" // _Py_EnterRecursiveCallTstate()
7+
#include "pycore_emscripten_trampoline.h" // descr_set_trampoline_call(), descr_get_trampoline_call()
78
#include "pycore_descrobject.h" // _PyMethodWrapper_Type
89
#include "pycore_object.h" // _PyObject_GC_UNTRACK()
910
#include "pycore_pystate.h" // _PyThreadState_GET()
@@ -16,25 +17,6 @@ class property "propertyobject *" "&PyProperty_Type"
1617
[clinic start generated code]*/
1718
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=556352653fd4c02e]*/
1819

19-
// see pycore_object.h
20-
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
21-
#include <emscripten.h>
22-
EM_JS(int, descr_set_trampoline_call, (setter set, PyObject *obj, PyObject *value, void *closure), {
23-
return wasmTable.get(set)(obj, value, closure);
24-
});
25-
26-
EM_JS(PyObject*, descr_get_trampoline_call, (getter get, PyObject *obj, void *closure), {
27-
return wasmTable.get(get)(obj, closure);
28-
});
29-
#else
30-
#define descr_set_trampoline_call(set, obj, value, closure) \
31-
(set)((obj), (value), (closure))
32-
33-
#define descr_get_trampoline_call(get, obj, closure) \
34-
(get)((obj), (closure))
35-
36-
#endif // __EMSCRIPTEN__ && PY_CALL_TRAMPOLINE
37-
3820
static void
3921
descr_dealloc(PyDescrObject *descr)
4022
{

Objects/methodobject.c

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -553,10 +553,3 @@ cfunction_call(PyObject *func, PyObject *args, PyObject *kwargs)
553553
return _Py_CheckFunctionResult(tstate, func, result, NULL);
554554
}
555555

556-
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
557-
#include <emscripten.h>
558-
559-
EM_JS(PyObject*, _PyCFunctionWithKeywords_TrampolineCall, (PyCFunctionWithKeywords func, PyObject *self, PyObject *args, PyObject *kw), {
560-
return wasmTable.get(func)(self, args, kw);
561-
});
562-
#endif

Python/emscripten_trampoline.c

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#if defined(PY_CALL_TRAMPOLINE)
2+
3+
#include <emscripten.h> // EM_JS
4+
#include <Python.h>
5+
#include "pycore_runtime.h" // _PyRuntime
6+
7+
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+
return "Function" in WebAssembly;
14+
});
15+
16+
void
17+
_Py_EmscriptenTrampoline_Init(_PyRuntimeState *runtime)
18+
{
19+
runtime->wasm_type_reflection_available = _PyEM_detect_type_reflection();
20+
}
21+
22+
/**
23+
* Backwards compatible trampoline works with all JS runtimes
24+
*/
25+
EM_JS(PyObject*,
26+
_PyEM_TrampolineCall_JavaScript, (PyCFunctionWithKeywords func,
27+
PyObject *arg1,
28+
PyObject *arg2,
29+
PyObject *arg3),
30+
{
31+
return wasmTable.get(func)(arg1, arg2, arg3);
32+
}
33+
);
34+
35+
/**
36+
* In runtimes with WebAssembly type reflection, count the number of parameters
37+
* and cast to the appropriate signature
38+
*/
39+
EM_JS(int, _PyEM_CountFuncParams, (PyCFunctionWithKeywords func),
40+
{
41+
let n = _PyEM_CountFuncParams.cache.get(func);
42+
43+
if (n !== undefined) {
44+
return n;
45+
}
46+
n = WebAssembly.Function.type(wasmTable.get(func)).parameters.length;
47+
_PyEM_CountFuncParams.cache.set(func, n);
48+
return n;
49+
}
50+
_PyEM_CountFuncParams.cache = new Map();
51+
)
52+
53+
54+
typedef PyObject* (*zero_arg)(void);
55+
typedef PyObject* (*one_arg)(PyObject*);
56+
typedef PyObject* (*two_arg)(PyObject*, PyObject*);
57+
typedef PyObject* (*three_arg)(PyObject*, PyObject*, PyObject*);
58+
59+
60+
PyObject*
61+
_PyEM_TrampolineCall_Reflection(PyCFunctionWithKeywords func,
62+
PyObject* self,
63+
PyObject* args,
64+
PyObject* kw)
65+
{
66+
switch (_PyEM_CountFuncParams(func)) {
67+
case 0:
68+
return ((zero_arg)func)();
69+
case 1:
70+
return ((one_arg)func)(self);
71+
case 2:
72+
return ((two_arg)func)(self, args);
73+
case 3:
74+
return ((three_arg)func)(self, args, kw);
75+
default:
76+
PyErr_SetString(PyExc_SystemError,
77+
"Handler takes too many arguments");
78+
return NULL;
79+
}
80+
}
81+
82+
#endif

Python/pystate.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "pycore_ceval.h"
66
#include "pycore_code.h" // stats
77
#include "pycore_dtoa.h" // _dtoa_state_INIT()
8+
#include "pycore_emscripten_trampoline.h" // _Py_EmscriptenTrampoline_Init()
89
#include "pycore_frame.h"
910
#include "pycore_initconfig.h" // _PyStatus_OK()
1011
#include "pycore_object.h" // _PyType_InitCache()
@@ -449,6 +450,10 @@ init_runtime(_PyRuntimeState *runtime,
449450

450451
runtime->unicode_state.ids.next_index = unicode_next_index;
451452

453+
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
454+
_Py_EmscriptenTrampoline_Init(runtime);
455+
#endif
456+
452457
runtime->_initialized = 1;
453458
}
454459

configure

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

configure.ac

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4593,8 +4593,8 @@ PLATFORM_OBJS=
45934593

45944594
AS_CASE([$ac_sys_system],
45954595
[Emscripten], [
4596-
AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o'])
4597-
AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h'])
4596+
AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o Python/emscripten_trampoline.o'])
4597+
AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h $(srcdir)/Include/internal/pycore_emscripten_trampoline.h'])
45984598
],
45994599
)
46004600
AC_SUBST([PLATFORM_HEADERS])

0 commit comments

Comments
 (0)