Skip to content

Commit a761e47

Browse files
hoodmaneambv
authored andcommitted
Make Emscripten trampolines work with JSPI
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. Upstreamed here: python#106219
1 parent 32155f0 commit a761e47

File tree

10 files changed

+159
-53
lines changed

10 files changed

+159
-53
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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_JS(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(meth, self, args, kw)) : \
47+
(_PyEM_TrampolineCall_JS(meth, self, args, kw)))
48+
49+
50+
#define _PyCFunction_TrampolineCall(meth, self, args) \
51+
_PyEM_TrampolineCall( \
52+
(*(PyCFunctionWithKeywords)(void(*)(void))(meth)), (self), (args), NULL)
53+
54+
#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \
55+
_PyEM_TrampolineCall((meth), (self), (args), (kw))
56+
57+
#else // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
58+
59+
#define _Py_EmscriptenTrampoline_Init(runtime)
60+
61+
#define _PyCFunction_TrampolineCall(meth, self, args) \
62+
(meth)((self), (args))
63+
#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \
64+
(meth)((self), (args), (kw))
65+
66+
#endif // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
67+
#endif // ndef Py_EMSCRIPTEN_SIGNAL_H

Include/internal/pycore_object.h

Lines changed: 1 addition & 27 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 "pycore_runtime.h" // _PyRuntime
@@ -417,33 +418,6 @@ extern int _PyObject_IsInstanceDictEmpty(PyObject *);
417418

418419
PyAPI_FUNC(PyObject *) _PyObject_LookupSpecial(PyObject *, PyObject *);
419420

420-
/* C function call trampolines to mitigate bad function pointer casts.
421-
*
422-
* Typical native ABIs ignore additional arguments or fill in missing
423-
* values with 0/NULL in function pointer cast. Compilers do not show
424-
* warnings when a function pointer is explicitly casted to an
425-
* incompatible type.
426-
*
427-
* Bad fpcasts are an issue in WebAssembly. WASM's indirect_call has strict
428-
* function signature checks. Argument count, types, and return type must
429-
* match.
430-
*
431-
* Third party code unintentionally rely on problematic fpcasts. The call
432-
* trampoline mitigates common occurrences of bad fpcasts on Emscripten.
433-
*/
434-
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
435-
#define _PyCFunction_TrampolineCall(meth, self, args) \
436-
_PyCFunctionWithKeywords_TrampolineCall( \
437-
(*(PyCFunctionWithKeywords)(void(*)(void))(meth)), (self), (args), NULL)
438-
extern PyObject* _PyCFunctionWithKeywords_TrampolineCall(
439-
PyCFunctionWithKeywords meth, PyObject *, PyObject *, PyObject *);
440-
#else
441-
#define _PyCFunction_TrampolineCall(meth, self, args) \
442-
(meth)((self), (args))
443-
#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \
444-
(meth)((self), (args), (kw))
445-
#endif // __EMSCRIPTEN__ && PY_CALL_TRAMPOLINE
446-
447421
#ifdef __cplusplus
448422
}
449423
#endif

Include/internal/pycore_runtime.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ typedef struct pyruntimestate {
7777
/* Is Python fully initialized? Set to 1 by Py_Initialize() */
7878
int initialized;
7979

80+
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
81+
/* Choose between trampoline based on type reflection vs based on EM_JS */
82+
int wasm_type_reflection_available;
83+
#endif
84+
8085
/* Set by Py_FinalizeEx(). Only reset to NULL if Py_Initialize()
8186
is called again.
8287

Objects/descrobject.c

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "Python.h"
44
#include "pycore_ceval.h" // _Py_EnterRecursiveCallTstate()
5+
#include "pycore_emscripten_trampoline.h" // _PyEM_TrampolineCall()
56
#include "pycore_object.h" // _PyObject_GC_UNTRACK()
67
#include "pycore_pystate.h" // _PyThreadState_GET()
78
#include "pycore_tuple.h" // _PyTuple_ITEMS()
@@ -14,24 +15,11 @@ class property "propertyobject *" "&PyProperty_Type"
1415
[clinic start generated code]*/
1516
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=556352653fd4c02e]*/
1617

17-
// see pycore_object.h
18-
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
19-
#include <emscripten.h>
20-
EM_JS(int, descr_set_trampoline_call, (setter set, PyObject *obj, PyObject *value, void *closure), {
21-
return wasmTable.get(set)(obj, value, closure);
22-
});
23-
24-
EM_JS(PyObject*, descr_get_trampoline_call, (getter get, PyObject *obj, void *closure), {
25-
return wasmTable.get(get)(obj, closure);
26-
});
27-
#else
2818
#define descr_set_trampoline_call(set, obj, value, closure) \
29-
(set)((obj), (value), (closure))
19+
((int)_PyEM_TrampolineCall((PyCFunctionWithKeywords)(set), (obj), (value), (PyObject*)(closure)))
3020

3121
#define descr_get_trampoline_call(get, obj, closure) \
32-
(get)((obj), (closure))
33-
34-
#endif // __EMSCRIPTEN__ && PY_CALL_TRAMPOLINE
22+
_PyEM_TrampolineCall((PyCFunctionWithKeywords)(get), (obj), (PyObject*)(closure), NULL)
3523

3624
static void
3725
descr_dealloc(PyDescrObject *descr)

Objects/methodobject.c

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

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

Python/emscripten_trampoline.c

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#if defined(PY_CALL_TRAMPOLINE)
2+
3+
#include <emscripten.h> // EM_JS, EM_JS_DEPS
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*, _PyEM_TrampolineCall_JS, (PyCFunctionWithKeywords func, PyObject *arg1, PyObject *arg2, PyObject *arg3), {
26+
return wasmTable.get(func)(arg1, arg2, arg3);
27+
}
28+
);
29+
30+
/**
31+
* In runtimes with WebAssembly type reflection, count the number of parameters
32+
* and cast to the appropriate signature
33+
*/
34+
EM_JS(int, _PyEM_CountFuncParams, (PyCFunctionWithKeywords func), {
35+
let n = _PyEM_CountFuncParams.cache.get(func);
36+
if (n !== undefined) {
37+
return n;
38+
}
39+
n = wasmFunctionType(wasmTable.get(func)).parameters.length;
40+
_PyEM_CountFuncParams.cache.set(func, n);
41+
return n;
42+
}
43+
_PyEM_CountFuncParams.cache = new Map();
44+
)
45+
46+
47+
typedef PyObject* (*zero_arg)(void);
48+
typedef PyObject* (*one_arg)(PyObject*);
49+
typedef PyObject* (*two_arg)(PyObject*, PyObject*);
50+
typedef PyObject* (*three_arg)(PyObject*, PyObject*, PyObject*);
51+
52+
53+
PyObject*
54+
_PyEM_TrampolineCall_Reflection(PyCFunctionWithKeywords func,
55+
PyObject* self,
56+
PyObject* args,
57+
PyObject* kw)
58+
{
59+
switch (_PyEM_CountFuncParams(func)) {
60+
case 0:
61+
return ((zero_arg)func)();
62+
case 1:
63+
return ((one_arg)func)(self);
64+
case 2:
65+
return ((two_arg)func)(self, args);
66+
case 3:
67+
return ((three_arg)func)(self, args, kw);
68+
default:
69+
PyErr_SetString(PyExc_SystemError, "Handler takes too many arguments");
70+
return NULL;
71+
}
72+
}
73+
74+
#endif

Python/pylifecycle.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "pycore_ceval.h" // _PyEval_FiniGIL()
66
#include "pycore_context.h" // _PyContext_Init()
7+
#include "pycore_emscripten_trampoline.h" // _Py_EmscriptenTrampoline_Init()
78
#include "pycore_exceptions.h" // _PyExc_InitTypes()
89
#include "pycore_dict.h" // _PyDict_Fini()
910
#include "pycore_fileutils.h" // _Py_ResetForceASCII()
@@ -539,7 +540,9 @@ pycore_init_runtime(_PyRuntimeState *runtime,
539540
if (_PyStatus_EXCEPTION(status)) {
540541
return status;
541542
}
543+
542544
return _PyStatus_OK();
545+
543546
}
544547

545548

Python/pystate.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/* Thread and interpreter state structures and their interfaces */
33

44
#include "Python.h"
5+
#include "pycore_emscripten_trampoline.h" // _Py_EmscriptenTrampoline_Init()
56
#include "pycore_ceval.h"
67
#include "pycore_code.h" // stats
78
#include "pycore_dtoa.h" // _dtoa_state_INIT()
@@ -451,6 +452,7 @@ init_runtime(_PyRuntimeState *runtime,
451452
runtime->unicode_state.ids.next_index = unicode_next_index;
452453

453454
runtime->_initialized = 1;
455+
_Py_EmscriptenTrampoline_Init(runtime);
454456
}
455457

456458
PyStatus

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
@@ -4837,8 +4837,8 @@ PLATFORM_OBJS=
48374837

48384838
AS_CASE([$ac_sys_system],
48394839
[Emscripten], [
4840-
AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o'])
4841-
AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h'])
4840+
AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o Python/emscripten_trampoline.o'])
4841+
AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h $(srcdir)/Include/internal/pycore_emscripten_trampoline.h'])
48424842
],
48434843
)
48444844
AC_SUBST([PLATFORM_HEADERS])

0 commit comments

Comments
 (0)