Skip to content

Commit

Permalink
gh-106213: Make Emscripten trampolines work with JSPI (GH-106219)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
3 people committed Sep 15, 2023
1 parent 59073c9 commit 6b179ad
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 30 deletions.
81 changes: 81 additions & 0 deletions Include/internal/pycore_emscripten_trampoline.h
@@ -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
1 change: 1 addition & 0 deletions Include/internal/pycore_object.h
Expand Up @@ -10,6 +10,7 @@ extern "C" {

#include <stdbool.h>
#include "pycore_gc.h" // _PyObject_GC_IS_TRACKED()
#include "pycore_emscripten_trampoline.h" // _PyCFunction_TrampolineCall()
#include "pycore_interp.h" // PyInterpreterState.gc
#include "pycore_pystate.h" // _PyInterpreterState_GET()

Expand Down
7 changes: 7 additions & 0 deletions Include/internal/pycore_runtime.h
Expand Up @@ -267,6 +267,13 @@ typedef struct pyruntimestate {

/* PyInterpreterState.interpreters.main */
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;


Expand Down
@@ -0,0 +1,2 @@
Changed the way that Emscripten call trampolines work for compatibility with
Wasm/JS Promise integration.
20 changes: 1 addition & 19 deletions Objects/descrobject.c
Expand Up @@ -4,6 +4,7 @@
#include "pycore_abstract.h" // _PyObject_RealIsSubclass()
#include "pycore_call.h" // _PyStack_AsDict()
#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_object.h" // _PyObject_GC_UNTRACK()
#include "pycore_pystate.h" // _PyThreadState_GET()
Expand All @@ -16,25 +17,6 @@ class property "propertyobject *" "&PyProperty_Type"
[clinic start generated code]*/
/*[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
descr_dealloc(PyDescrObject *descr)
{
Expand Down
7 changes: 0 additions & 7 deletions Objects/methodobject.c
Expand Up @@ -553,10 +553,3 @@ cfunction_call(PyObject *func, PyObject *args, PyObject *kwargs)
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
82 changes: 82 additions & 0 deletions Python/emscripten_trampoline.c
@@ -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
5 changes: 5 additions & 0 deletions Python/pystate.c
Expand Up @@ -5,6 +5,7 @@
#include "pycore_ceval.h"
#include "pycore_code.h" // stats
#include "pycore_dtoa.h" // _dtoa_state_INIT()
#include "pycore_emscripten_trampoline.h" // _Py_EmscriptenTrampoline_Init()
#include "pycore_frame.h"
#include "pycore_initconfig.h" // _PyStatus_OK()
#include "pycore_object.h" // _PyType_InitCache()
Expand Down Expand Up @@ -449,6 +450,10 @@ init_runtime(_PyRuntimeState *runtime,

runtime->unicode_state.ids.next_index = unicode_next_index;

#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
_Py_EmscriptenTrampoline_Init(runtime);
#endif

runtime->_initialized = 1;
}

Expand Down
4 changes: 2 additions & 2 deletions configure

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions configure.ac
Expand Up @@ -4593,8 +4593,8 @@ PLATFORM_OBJS=

AS_CASE([$ac_sys_system],
[Emscripten], [
AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o'])
AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h'])
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 $(srcdir)/Include/internal/pycore_emscripten_trampoline.h'])
],
)
AC_SUBST([PLATFORM_HEADERS])
Expand Down

0 comments on commit 6b179ad

Please sign in to comment.