Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-128627: Emscripten: Use wasm-gc based call adaptor if available #128628

Merged
merged 12 commits into from
Jan 12, 2025
24 changes: 4 additions & 20 deletions Include/internal/pycore_emscripten_trampoline.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,11 @@
*/

#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))))
_PyEM_TrampolineCall(PyCFunctionWithKeywords func,
PyObject* self,
PyObject* args,
PyObject* kw);

#define _PyCFunction_TrampolineCall(meth, self, args) \
_PyEM_TrampolineCall( \
Expand All @@ -62,8 +48,6 @@ _PyEM_TrampolineCall_Reflection(PyCFunctionWithKeywords func,

#else // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)

#define _Py_EmscriptenTrampoline_Init(runtime)

#define _PyCFunction_TrampolineCall(meth, self, args) \
(meth)((self), (args))

Expand Down
252 changes: 186 additions & 66 deletions Python/emscripten_trampoline.c
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file seems to be using 2-space indenting rather than 4-space for all other C code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've marked this resolved... but it looks like you've gone the opposite direction to what I was intending/suggesting. Python's C code is 4-space indented; the updates you've made here move everything to 2-space. It should be 4-space for consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry I misread your comment. It's hard to remember all these different code styles.

Original file line number Diff line number Diff line change
@@ -1,92 +1,212 @@
#if defined(PY_CALL_TRAMPOLINE)

#include <emscripten.h> // EM_JS
#include <emscripten.h> // EM_JS, EM_JS_DEPS
#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, (), {
if (!("Function" in WebAssembly)) {
return false;
}
if (WebAssembly.Function.type) {
// Node v20
Module.PyEM_CountArgs = (func) => WebAssembly.Function.type(wasmTable.get(func)).parameters.length;
} else {
// Node >= 22, v8-based browsers
Module.PyEM_CountArgs = (func) => wasmTable.get(func).type().parameters.length;
}
return true;
});

void
_Py_EmscriptenTrampoline_Init(_PyRuntimeState *runtime)
{
runtime->wasm_type_reflection_available = _PyEM_detect_type_reflection();
}
typedef int (*CountArgsFunc)(PyCFunctionWithKeywords func);
// We have to be careful to work correctly with memory snapshots. Even if we are
// loading a memory snapshot, we need to perform the JS initialization work.
// That means we can't call the initialization code from C. Instead, we export
// this function pointer to JS and then fill it in a preRun function which runs
// unconditionally.
EMSCRIPTEN_KEEPALIVE CountArgsFunc _PyEM_CountFuncParams = NULL;
hoodmane marked this conversation as resolved.
Show resolved Hide resolved

/**
* Backwards compatible trampoline works with all JS runtimes
*/
EM_JS(PyObject*,
_PyEM_TrampolineCall_JavaScript, (PyCFunctionWithKeywords func,
PyObject *arg1,
PyObject *arg2,
PyObject *arg3),
{
EM_JS(PyObject*, _PyEM_TrampolineCall_JS, (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);
// Binary module for the checks. It has to be done in web assembly because
// clang/llvm have no support yet for the reference types yet. In fact, the wasm
// binary toolkit doesn't yet support the ref.test instruction either. To
// convert the following module to the binary, my approach is to find and
// replace "ref.test $type" -> "drop i32.const n" on the source text. This
// results in the bytes "0x1a, 0x41, n" where we need the bytes "0xfb, 0x14, n"
// so doing a find and replace on the output from "0x1a, 0x41" -> "0xfb, 0x14"
// gets us the output we need.
//
// (module
// (type $type0 (func (param) (result i32)))
// (type $type1 (func (param i32) (result i32)))
// (type $type2 (func (param i32 i32) (result i32)))
// (type $type3 (func (param i32 i32 i32) (result i32)))
// (type $type4 (func (param i32 i32 i32 i32) (result i32)))
// (type $blocktype (func (param i32) (result)))
// (table $funcs (import "e" "t") 0 funcref)
// (export "f" (func $f))
// (func $f (param $fptr i32) (result i32)
// (local $fref funcref)
// local.get $fptr
// table.get $funcs
// local.tee $fref
// ref.test $type4
// (block $b (type $blocktype)
// i32.eqz
// br_if $b
// i32.const 4
// return
// )
// local.get $fref
// ref.test $type3
// (block $b (type $blocktype)
// i32.eqz
// br_if $b
// i32.const 3
// return
// )
// local.get $fref
// ref.test $type2
// (block $b (type $blocktype)
// i32.eqz
// br_if $b
// i32.const 2
// return
// )
// ref.test $type1
// i32.const 1
// (block $b (type $blocktype)
// i32.eqz
// br_if $b
// i32.const 1
// return
// )
// ref.test $type0
// i32.const 0
// (block $b (type $blocktype)
// i32.eqz
// br_if $b
// i32.const 0
// return
// )
// i32.const -1
// )
// )
addOnPreRun(() => {
// Try to initialize countArgsFunc
const code = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, // \0asm magic number
0x01, 0x00, 0x00, 0x00, // version 1
0x01, 0x23, // Type section, body is 0x23 bytes
0x06, // 6 entries
0x60, 0x00, 0x01, 0x7f, // (type $type0 (func (param) (result i32)))
0x60, 0x01, 0x7f, 0x01, 0x7f, // (type $type1 (func (param i32) (result i32)))
0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, // (type $type2 (func (param i32 i32) (result i32)))
0x60, 0x03, 0x7f, 0x7f, 0x7f, 0x01, 0x7f, // (type $type3 (func (param i32 i32 i32) (result i32)))
0x60, 0x04, 0x7f, 0x7f, 0x7f, 0x7f, 0x01, 0x7f, // (type $type4 (func (param i32 i32 i32 i32) (result i32)))
0x60, 0x01, 0x7f, 0x00, // (type $blocktype (func (param i32) (result)))
0x02, 0x09, // Import section, 0x9 byte body
0x01, // 1 import (table $funcs (import "e" "t") 0 funcref)
0x01, 0x65, // "e"
0x01, 0x74, // "t"
0x01, // importing a table
0x70, // of entry type funcref
0x00, 0x00, // table limits: no max, min of 0
0x03, 0x02, // Function section
0x01, 0x01, // We're going to define one function of type 1 (func (param i32) (result i32))
0x07, 0x05, // export section
0x01, // 1 export
0x01, 0x66, // called "f"
0x00, // a function
0x00, // at index 0

if (n !== undefined) {
return n;
}
n = Module.PyEM_CountArgs(func);
_PyEM_CountFuncParams.cache.set(func, n);
return n;
}
_PyEM_CountFuncParams.cache = new Map();
)
0x0a, 0x52, // Code section,
0x01, 0x50, // one entry of length 50
0x01, 0x01, 0x70, // one local of type funcref
// Body of the function
0x20, 0x00, // local.get $fptr
0x25, 0x00, // table.get $funcs
0x22, 0x01, // local.tee $fref
0xfb, 0x14, 0x04, // ref.test $type4
0x02, 0x05, // block $b (type $blocktype)
0x45, // i32.eqz
0x0d, 0x00, // br_if $b
0x41, 0x04, // i32.const 4
0x0f, // return
0x0b, // end block

0x20, 0x01, // local.get $fref
0xfb, 0x14, 0x03, // ref.test $type3
0x02, 0x05, // block $b (type $blocktype)
0x45, // i32.eqz
0x0d, 0x00, // br_if $b
0x41, 0x03, // i32.const 3
0x0f, // return
0x0b, // end block

0x20, 0x01, // local.get $fref
0xfb, 0x14, 0x02, // ref.test $type2
0x02, 0x05, // block $b (type $blocktype)
0x45, // i32.eqz
0x0d, 0x00, // br_if $b
0x41, 0x02, // i32.const 2
0x0f, // return
0x0b, // end block

0x20, 0x01, // local.get $fref
0xfb, 0x14, 0x01, // ref.test $type1
0x02, 0x05, // block $b (type $blocktype)
0x45, // i32.eqz
0x0d, 0x00, // br_if $b
0x41, 0x01, // i32.const 1
0x0f, // return
0x0b, // end block

0x20, 0x01, // local.get $fref
0xfb, 0x14, 0x00, // ref.test $type0
0x02, 0x05, // block $b (type $blocktype)
0x45, // i32.eqz
0x0d, 0x00, // br_if $b
0x41, 0x00, // i32.const 0
0x0f, // return
0x0b, // end block

0x41, 0x7f, // i32.const -1
0x0b // end function
]);
let ptr = 0;
try {
const mod = new WebAssembly.Module(code);
const inst = new WebAssembly.Instance(mod, {e: {t: wasmTable}});
ptr = addFunction(inst.exports.f);
} catch(e) {
// If something goes wrong, we'll null out _PyEM_CountFuncParams and fall
// back to the JS trampoline.
}
HEAP32[__PyEM_CountFuncParams/4] = ptr;
});
);

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)
_PyEM_TrampolineCall(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;
}
if (_PyEM_CountFuncParams == 0) {
return _PyEM_TrampolineCall_JS(func, self, args, 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
1 change: 1 addition & 0 deletions Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "pycore_ceval.h" // _PyEval_FiniGIL()
#include "pycore_codecs.h" // _PyCodec_Lookup()
#include "pycore_context.h" // _PyContext_Init()
#include "pycore_exceptions.h" // _PyExc_InitTypes()
#include "pycore_dict.h" // _PyDict_Fini()
#include "pycore_exceptions.h" // _PyExc_InitTypes()
#include "pycore_fileutils.h" // _Py_ResetForceASCII()
Expand Down
5 changes: 0 additions & 5 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
#include "pycore_code.h" // stats
#include "pycore_critical_section.h" // _PyCriticalSection_Resume()
#include "pycore_dtoa.h" // _dtoa_state_INIT()
#include "pycore_emscripten_trampoline.h" // _Py_EmscriptenTrampoline_Init()
#include "pycore_frame.h"
#include "pycore_freelist.h" // _PyObject_ClearFreeLists()
#include "pycore_initconfig.h" // _PyStatus_OK()
Expand Down Expand Up @@ -432,10 +431,6 @@ 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
Loading