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 <thomas@python.org>
Co-authored-by: Brett Cannon <brett@python.org>
This commit is contained in:
Hood Chatham 2023-09-15 15:04:21 -07:00 committed by GitHub
parent 59073c9ab8
commit 6b179adb8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 183 additions and 30 deletions

View File

@ -0,0 +1,81 @@
#ifndef Py_EMSCRIPTEN_TRAMPOLINE_H
#define Py_EMSCRIPTEN_TRAMPOLINE_H
#include "pycore_runtime.h" // _PyRuntimeState
/**
* C function call trampolines to mitigate bad function pointer casts.
*
* Section 6.3.2.3, paragraph 8 reads:
*
* A pointer to a function of one type may be converted to a pointer to a
* function of another type and back again; the result shall compare equal to
* the original pointer. If a converted pointer is used to call a function
* whose type is not compatible with the pointed-to type, the behavior is
* undefined.
*
* Typical native ABIs ignore additional arguments or fill in missing values
* with 0/NULL in function pointer cast. Compilers do not show warnings when a
* function pointer is explicitly casted to an incompatible type.
*
* Bad fpcasts are an issue in WebAssembly. WASM's indirect_call has strict
* function signature checks. Argument count, types, and return type must match.
*
* Third party code unintentionally rely on problematic fpcasts. The call
* trampoline mitigates common occurrences of bad fpcasts on Emscripten.
*/
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
void _Py_EmscriptenTrampoline_Init(_PyRuntimeState *runtime);
PyObject*
_PyEM_TrampolineCall_JavaScript(PyCFunctionWithKeywords func,
PyObject* self,
PyObject* args,
PyObject* kw);
PyObject*
_PyEM_TrampolineCall_Reflection(PyCFunctionWithKeywords func,
PyObject* self,
PyObject* args,
PyObject* kw);
#define _PyEM_TrampolineCall(meth, self, args, kw) \
((_PyRuntime.wasm_type_reflection_available) ? \
(_PyEM_TrampolineCall_Reflection((PyCFunctionWithKeywords)(meth), (self), (args), (kw))) : \
(_PyEM_TrampolineCall_JavaScript((PyCFunctionWithKeywords)(meth), (self), (args), (kw))))
#define _PyCFunction_TrampolineCall(meth, self, args) \
_PyEM_TrampolineCall( \
(*(PyCFunctionWithKeywords)(void(*)(void))(meth)), (self), (args), NULL)
#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \
_PyEM_TrampolineCall((meth), (self), (args), (kw))
#define descr_set_trampoline_call(set, obj, value, closure) \
((int)_PyEM_TrampolineCall((PyCFunctionWithKeywords)(set), (obj), (value), (PyObject*)(closure)))
#define descr_get_trampoline_call(get, obj, closure) \
_PyEM_TrampolineCall((PyCFunctionWithKeywords)(get), (obj), (PyObject*)(closure), NULL)
#else // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
#define _Py_EmscriptenTrampoline_Init(runtime)
#define _PyCFunction_TrampolineCall(meth, self, args) \
(meth)((self), (args))
#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \
(meth)((self), (args), (kw))
#define descr_set_trampoline_call(set, obj, value, closure) \
(set)((obj), (value), (closure))
#define descr_get_trampoline_call(get, obj, closure) \
(get)((obj), (closure))
#endif // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
#endif // ndef Py_EMSCRIPTEN_SIGNAL_H

View File

@ -10,6 +10,7 @@ extern "C" {
#include <stdbool.h> #include <stdbool.h>
#include "pycore_gc.h" // _PyObject_GC_IS_TRACKED() #include "pycore_gc.h" // _PyObject_GC_IS_TRACKED()
#include "pycore_emscripten_trampoline.h" // _PyCFunction_TrampolineCall()
#include "pycore_interp.h" // PyInterpreterState.gc #include "pycore_interp.h" // PyInterpreterState.gc
#include "pycore_pystate.h" // _PyInterpreterState_GET() #include "pycore_pystate.h" // _PyInterpreterState_GET()

View File

@ -267,6 +267,13 @@ typedef struct pyruntimestate {
/* PyInterpreterState.interpreters.main */ /* PyInterpreterState.interpreters.main */
PyInterpreterState _main_interpreter; PyInterpreterState _main_interpreter;
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
// Used in "Python/emscripten_trampoline.c" to choose between type
// reflection trampoline and EM_JS trampoline.
bool wasm_type_reflection_available;
#endif
} _PyRuntimeState; } _PyRuntimeState;

View File

@ -0,0 +1,2 @@
Changed the way that Emscripten call trampolines work for compatibility with
Wasm/JS Promise integration.

View File

@ -4,6 +4,7 @@
#include "pycore_abstract.h" // _PyObject_RealIsSubclass() #include "pycore_abstract.h" // _PyObject_RealIsSubclass()
#include "pycore_call.h" // _PyStack_AsDict() #include "pycore_call.h" // _PyStack_AsDict()
#include "pycore_ceval.h" // _Py_EnterRecursiveCallTstate() #include "pycore_ceval.h" // _Py_EnterRecursiveCallTstate()
#include "pycore_emscripten_trampoline.h" // descr_set_trampoline_call(), descr_get_trampoline_call()
#include "pycore_descrobject.h" // _PyMethodWrapper_Type #include "pycore_descrobject.h" // _PyMethodWrapper_Type
#include "pycore_object.h" // _PyObject_GC_UNTRACK() #include "pycore_object.h" // _PyObject_GC_UNTRACK()
#include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_pystate.h" // _PyThreadState_GET()
@ -16,25 +17,6 @@ class property "propertyobject *" "&PyProperty_Type"
[clinic start generated code]*/ [clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=556352653fd4c02e]*/ /*[clinic end generated code: output=da39a3ee5e6b4b0d input=556352653fd4c02e]*/
// see pycore_object.h
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
#include <emscripten.h>
EM_JS(int, descr_set_trampoline_call, (setter set, PyObject *obj, PyObject *value, void *closure), {
return wasmTable.get(set)(obj, value, closure);
});
EM_JS(PyObject*, descr_get_trampoline_call, (getter get, PyObject *obj, void *closure), {
return wasmTable.get(get)(obj, closure);
});
#else
#define descr_set_trampoline_call(set, obj, value, closure) \
(set)((obj), (value), (closure))
#define descr_get_trampoline_call(get, obj, closure) \
(get)((obj), (closure))
#endif // __EMSCRIPTEN__ && PY_CALL_TRAMPOLINE
static void static void
descr_dealloc(PyDescrObject *descr) descr_dealloc(PyDescrObject *descr)
{ {

View File

@ -553,10 +553,3 @@ cfunction_call(PyObject *func, PyObject *args, PyObject *kwargs)
return _Py_CheckFunctionResult(tstate, func, result, NULL); return _Py_CheckFunctionResult(tstate, func, result, NULL);
} }
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
#include <emscripten.h>
EM_JS(PyObject*, _PyCFunctionWithKeywords_TrampolineCall, (PyCFunctionWithKeywords func, PyObject *self, PyObject *args, PyObject *kw), {
return wasmTable.get(func)(self, args, kw);
});
#endif

View File

@ -0,0 +1,82 @@
#if defined(PY_CALL_TRAMPOLINE)
#include <emscripten.h> // EM_JS
#include <Python.h>
#include "pycore_runtime.h" // _PyRuntime
/**
* This is the GoogleChromeLabs approved way to feature detect type-reflection:
* https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/type-reflection/index.js
*/
EM_JS(int, _PyEM_detect_type_reflection, (), {
return "Function" in WebAssembly;
});
void
_Py_EmscriptenTrampoline_Init(_PyRuntimeState *runtime)
{
runtime->wasm_type_reflection_available = _PyEM_detect_type_reflection();
}
/**
* Backwards compatible trampoline works with all JS runtimes
*/
EM_JS(PyObject*,
_PyEM_TrampolineCall_JavaScript, (PyCFunctionWithKeywords func,
PyObject *arg1,
PyObject *arg2,
PyObject *arg3),
{
return wasmTable.get(func)(arg1, arg2, arg3);
}
);
/**
* In runtimes with WebAssembly type reflection, count the number of parameters
* and cast to the appropriate signature
*/
EM_JS(int, _PyEM_CountFuncParams, (PyCFunctionWithKeywords func),
{
let n = _PyEM_CountFuncParams.cache.get(func);
if (n !== undefined) {
return n;
}
n = WebAssembly.Function.type(wasmTable.get(func)).parameters.length;
_PyEM_CountFuncParams.cache.set(func, n);
return n;
}
_PyEM_CountFuncParams.cache = new Map();
)
typedef PyObject* (*zero_arg)(void);
typedef PyObject* (*one_arg)(PyObject*);
typedef PyObject* (*two_arg)(PyObject*, PyObject*);
typedef PyObject* (*three_arg)(PyObject*, PyObject*, PyObject*);
PyObject*
_PyEM_TrampolineCall_Reflection(PyCFunctionWithKeywords func,
PyObject* self,
PyObject* args,
PyObject* kw)
{
switch (_PyEM_CountFuncParams(func)) {
case 0:
return ((zero_arg)func)();
case 1:
return ((one_arg)func)(self);
case 2:
return ((two_arg)func)(self, args);
case 3:
return ((three_arg)func)(self, args, kw);
default:
PyErr_SetString(PyExc_SystemError,
"Handler takes too many arguments");
return NULL;
}
}
#endif

View File

@ -5,6 +5,7 @@
#include "pycore_ceval.h" #include "pycore_ceval.h"
#include "pycore_code.h" // stats #include "pycore_code.h" // stats
#include "pycore_dtoa.h" // _dtoa_state_INIT() #include "pycore_dtoa.h" // _dtoa_state_INIT()
#include "pycore_emscripten_trampoline.h" // _Py_EmscriptenTrampoline_Init()
#include "pycore_frame.h" #include "pycore_frame.h"
#include "pycore_initconfig.h" // _PyStatus_OK() #include "pycore_initconfig.h" // _PyStatus_OK()
#include "pycore_object.h" // _PyType_InitCache() #include "pycore_object.h" // _PyType_InitCache()
@ -449,6 +450,10 @@ init_runtime(_PyRuntimeState *runtime,
runtime->unicode_state.ids.next_index = unicode_next_index; runtime->unicode_state.ids.next_index = unicode_next_index;
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
_Py_EmscriptenTrampoline_Init(runtime);
#endif
runtime->_initialized = 1; runtime->_initialized = 1;
} }

4
configure generated vendored
View File

@ -16941,8 +16941,8 @@ PLATFORM_OBJS=
case $ac_sys_system in #( case $ac_sys_system in #(
Emscripten) : Emscripten) :
as_fn_append PLATFORM_OBJS ' Python/emscripten_signal.o' as_fn_append PLATFORM_OBJS ' Python/emscripten_signal.o Python/emscripten_trampoline.o'
as_fn_append PLATFORM_HEADERS ' $(srcdir)/Include/internal/pycore_emscripten_signal.h' as_fn_append PLATFORM_HEADERS ' $(srcdir)/Include/internal/pycore_emscripten_signal.h $(srcdir)/Include/internal/pycore_emscripten_trampoline.h'
;; #( ;; #(
*) : *) :
;; ;;

View File

@ -4593,8 +4593,8 @@ PLATFORM_OBJS=
AS_CASE([$ac_sys_system], AS_CASE([$ac_sys_system],
[Emscripten], [ [Emscripten], [
AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o']) AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o Python/emscripten_trampoline.o'])
AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h']) AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h $(srcdir)/Include/internal/pycore_emscripten_trampoline.h'])
], ],
) )
AC_SUBST([PLATFORM_HEADERS]) AC_SUBST([PLATFORM_HEADERS])