diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index 3ecc5e9f0554b09..dbc0ebf994fd1e0 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -8,7 +8,7 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif -#include "pycore_emscripten_trampoline.h" // _PyCFunction_TrampolineCall() +#include "pycore_wasm_trampoline.h" // _PyCFunction_TrampolineCall() #include "pycore_gc.h" // _PyObject_GC_TRACK() #include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_PTR_ACQUIRE() #include "pycore_pystate.h" // _PyInterpreterState_GET() @@ -977,27 +977,6 @@ extern PyObject* _PyObject_NextNotImplemented(PyObject *); // Export for '_datetime' shared extension PyAPI_FUNC(PyObject*) _PyObject_GetState(PyObject *); -/* C function call trampolines to mitigate bad function pointer casts. - * - * 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)) -#define _PyCFunction_TrampolineCall(meth, self, args) \ - (meth)((self), (args)) -#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \ - (meth)((self), (args), (kw)) -#endif // __EMSCRIPTEN__ && PY_CALL_TRAMPOLINE - // Export these 2 symbols for '_pickle' shared extension PyAPI_DATA(PyTypeObject) _PyNone_Type; PyAPI_DATA(PyTypeObject) _PyNotImplemented_Type; diff --git a/Include/internal/pycore_runtime_structs.h b/Include/internal/pycore_runtime_structs.h index 145e66de9984ca7..5470f5858eda087 100644 --- a/Include/internal/pycore_runtime_structs.h +++ b/Include/internal/pycore_runtime_structs.h @@ -271,16 +271,6 @@ struct pyruntimestate { struct _types_runtime_state types; struct _Py_time_runtime_state time; -#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE) - // Used in "Python/emscripten_trampoline.c" to choose between wasm-gc - // trampoline and JavaScript trampoline. - PyObject* (*emscripten_trampoline)(int* success, - PyCFunctionWithKeywords func, - PyObject* self, - PyObject* args, - PyObject* kw); -#endif - /* All the objects that are shared by the runtime's interpreters. */ struct _Py_cached_objects cached_objects; struct _Py_static_objects static_objects; diff --git a/Include/internal/pycore_emscripten_trampoline.h b/Include/internal/pycore_wasm_trampoline.h similarity index 56% rename from Include/internal/pycore_emscripten_trampoline.h rename to Include/internal/pycore_wasm_trampoline.h index e37c53a64f4a723..1199bab0aca0976 100644 --- a/Include/internal/pycore_emscripten_trampoline.h +++ b/Include/internal/pycore_wasm_trampoline.h @@ -1,7 +1,7 @@ -#ifndef Py_EMSCRIPTEN_TRAMPOLINE_H -#define Py_EMSCRIPTEN_TRAMPOLINE_H +#ifndef Py_WASM_TRAMPOLINE_H +#define Py_WASM_TRAMPOLINE_H -#include "pycore_typedefs.h" // _PyRuntimeState +#include "Python.h" /** * C function call trampolines to mitigate bad function pointer casts. @@ -18,37 +18,44 @@ * 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 + * 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. + * trampoline mitigates common occurrences of bad fpcasts on Wasm targets. */ -#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE) +#if defined(__wasm__) && defined(PY_CALL_TRAMPOLINE) PyObject* -_PyEM_TrampolineCall(PyCFunctionWithKeywords func, - PyObject* self, - PyObject* args, - PyObject* kw); +_PyWasm_TrampolineCall(PyCFunctionWithKeywords func, + PyObject* self, + PyObject* args, + PyObject* kw); + +int +_PyWasm_TrampolineCallSetter(setter func, + PyObject* self, + PyObject* val, + void* closure); #define _PyCFunction_TrampolineCall(meth, self, args) \ - _PyEM_TrampolineCall(*_PyCFunctionWithKeywords_CAST(meth), (self), (args), NULL) + _PyWasm_TrampolineCall(*_PyCFunctionWithKeywords_CAST(meth), (self), (args), NULL) #define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \ - _PyEM_TrampolineCall((meth), (self), (args), (kw)) + _PyWasm_TrampolineCall((meth), (self), (args), (kw)) #define descr_set_trampoline_call(set, obj, value, closure) \ - ((int)_PyEM_TrampolineCall(_PyCFunctionWithKeywords_CAST(set), (obj), \ - (value), (PyObject*)(closure))) + _PyWasm_TrampolineCallSetter((set), (obj), (value), (closure)) #define descr_get_trampoline_call(get, obj, closure) \ - _PyEM_TrampolineCall(_PyCFunctionWithKeywords_CAST(get), (obj), \ - (PyObject*)(closure), NULL) + _PyWasm_TrampolineCall(_PyCFunctionWithKeywords_CAST(get), (obj), \ + (PyObject*)(closure), NULL) + +#else // defined(__wasm__) && defined(PY_CALL_TRAMPOLINE) -#else // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE) +// Disable trampolines by directly calling the method. #define _PyCFunction_TrampolineCall(meth, self, args) \ (meth)((self), (args)) @@ -62,6 +69,6 @@ _PyEM_TrampolineCall(PyCFunctionWithKeywords func, #define descr_get_trampoline_call(get, obj, closure) \ (get)((obj), (closure)) -#endif // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE) +#endif // defined(__wasm__) && defined(PY_CALL_TRAMPOLINE) -#endif // ndef Py_EMSCRIPTEN_SIGNAL_H +#endif // ndef Py_WASM_TRAMPOLINE_H diff --git a/Lib/test/test_capi/test_fpcast.py b/Lib/test/test_capi/test_fpcast.py new file mode 100644 index 000000000000000..ae19ac11f6a191f --- /dev/null +++ b/Lib/test/test_capi/test_fpcast.py @@ -0,0 +1,77 @@ +"""Tests for calling C functions through mis-cast function pointers. + +Extension modules frequently cast C functions with the wrong number of +arguments to PyCFunction, getter, setter or ternaryfunc. Native ABIs tolerate +this; on WebAssembly, call_indirect checks the signature, so CPython routes +these calls through a trampoline (Python/wasm_trampoline.c) that detects the +real signature. Calling each variant must work everywhere. +""" +import unittest +from test.support import check_sanitizer, import_helper, is_wasm32 + +_testcapi = import_helper.import_module('_testcapi') + + +# Native ABIs tolerate these calls but they are implementation-defined behavior. +# -fsanitize=undefined correctly trips on them. +@unittest.skipIf(check_sanitizer(ub=True), + "calls through mis-cast function pointers are UB natively") +class FpcastTest(unittest.TestCase): + def check_calls(self, obj, prefix): + for arity in range(4): + with self.subTest(kind="noargs", arity=arity): + self.assertIsNone(getattr(obj, f"{prefix}noargs{arity}")()) + with self.subTest(kind="o", arity=arity): + self.assertIsNone(getattr(obj, f"{prefix}o{arity}")(1)) + with self.subTest(kind="varargs", arity=arity): + self.assertIsNone(getattr(obj, f"{prefix}varargs{arity}")()) + self.assertIsNone(getattr(obj, f"{prefix}varargs{arity}")(1, 2)) + with self.subTest(kind="kwargs", arity=arity): + self.assertIsNone(getattr(obj, f"{prefix}kwargs{arity}")()) + self.assertIsNone(getattr(obj, f"{prefix}kwargs{arity}")(1, x=2)) + + def test_module_functions(self): + self.check_calls(_testcapi, "fpcast_") + + def test_methods(self): + self.check_calls(_testcapi.FpcastTestType(), "") + + def test_tp_call(self): + for arity in range(4): + with self.subTest(arity=arity): + cls = getattr(_testcapi, f"FpcastCallable{arity}") + self.assertIsNone(cls()()) + self.assertIsNone(cls()(1, x=2)) + + def test_getset(self): + t = _testcapi.FpcastTestType() + self.assertIsNone(t.getset0) + self.assertIsNone(t.getset1) + self.assertIsNone(t.getset2) + t.getset1 = 5 + sentinel = object() + t.getset2 = sentinel + self.assertIs(_testcapi.fpcast_last_set_value(), sentinel) + with self.assertRaises(AttributeError): + t.getset0 = 1 + + @unittest.skipUnless(is_wasm32, "requires the wasm call trampoline") + def test_unsupported_signature(self): + # A function with four pointer arguments matches none of the + # signatures the trampoline knows about: it must raise SystemError + # rather than trap. + t = _testcapi.FpcastTestType() + with self.assertRaises(SystemError): + _testcapi.fpcast_noargs4() + with self.assertRaises(SystemError): + t.noargs4() + with self.assertRaises(SystemError): + _testcapi.FpcastCallable4()() + with self.assertRaises(SystemError): + t.getset4 + with self.assertRaises(SystemError): + t.getset4 = 1 + + +if __name__ == "__main__": + unittest.main() diff --git a/Makefile.pre.in b/Makefile.pre.in index 42480f28ad7f315..36515dbd146a1e3 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -1439,6 +1439,7 @@ PYTHON_HEADERS= \ $(srcdir)/Include/internal/pycore_uop.h \ $(srcdir)/Include/internal/pycore_uop_ids.h \ $(srcdir)/Include/internal/pycore_uop_metadata.h \ + $(srcdir)/Include/internal/pycore_wasm_trampoline.h \ $(srcdir)/Include/internal/pycore_warnings.h \ $(srcdir)/Include/internal/pycore_weakref.h \ $(DTRACE_HEADERS) \ @@ -3153,13 +3154,6 @@ Python/asm_trampoline_universal2.o: $(srcdir)/Python/asm_trampoline_aarch64.S $( rm -f Python/asm_trampoline_arm64-apple-darwin.o \ Python/asm_trampoline_x86_64-apple-darwin.o -Python/emscripten_trampoline_inner.wasm: $(srcdir)/Python/emscripten_trampoline_inner.c - # emcc has a path that ends with emsdk/upstream/emscripten/emcc, we're looking for emsdk/upstream/bin/clang. - $$(em-config LLVM_ROOT)/clang -o $@ $< -mgc -O2 -Wl,--no-entry -Wl,--import-table -Wl,--import-memory -target wasm32-unknown-unknown -nostdlib - -Python/emscripten_trampoline_wasm.c: Python/emscripten_trampoline_inner.wasm - $(PYTHON_FOR_REGEN) $(srcdir)/Platforms/emscripten/prepare_external_wasm.py $< $@ getWasmTrampolineModule - JIT_SHIM_BUILD_OBJS= @JIT_SHIM_BUILD_O@ JIT_UNWIND_INFO_H= $(if $(JIT_OBJS),jit_unwind_info.h $(patsubst jit_stencils-%.h,jit_unwind_info-%.h,@JIT_STENCILS_H@)) JIT_BUILD_TARGETS= jit_stencils.h @JIT_STENCILS_H@ $(JIT_UNWIND_INFO_H) $(JIT_SHIM_BUILD_OBJS) diff --git a/Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst b/Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst new file mode 100644 index 000000000000000..50f432d71e22f30 --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst @@ -0,0 +1,3 @@ +Added support for function call adaptor trampolines for wasi to prevent +traps when C handlers take the wrong number of arguments. Dropped Emscripten +function call adaptors support for JS runtimes that don't support wasm-gc. diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index dd9f38e59f7a3ff..d8cd8de10dfe084 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -173,7 +173,7 @@ @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c @MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tokenizer.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c -@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c _testcapi/marshal.c +@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c _testcapi/marshal.c _testcapi/fpcast.c @MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/capsule.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slice.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c _testlimitedcapi/type.c _testlimitedcapi/hash.c _testlimitedcapi/build.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c diff --git a/Modules/_testcapi/fpcast.c b/Modules/_testcapi/fpcast.c new file mode 100644 index 000000000000000..ed6e2d3c5c5e6d9 --- /dev/null +++ b/Modules/_testcapi/fpcast.c @@ -0,0 +1,164 @@ +/* + * Tests for Python/wasm_trampoline.c + * + * Third party extensions frequently cast functions with the "wrong" number of + * arguments to PyCFunction, getter, setter or ternaryfunc. On native targets + * this works by accident. On WebAssembly, call_indirect checks the signature + * and traps, so CPython routes these calls through a trampoline that detects + * the real signature. + */ + +#include "parts.h" + +static PyObject * +zero(void) +{ + Py_RETURN_NONE; +} + +static PyObject * +one(PyObject *self) +{ + Py_RETURN_NONE; +} + +static PyObject * +two(PyObject *self, PyObject *args) +{ + Py_RETURN_NONE; +} + +static PyObject * +three(PyObject *self, PyObject *args, PyObject *kwargs) +{ + Py_RETURN_NONE; +} + +// Using this as a handler should raise a SystemError. +static PyObject * +four(PyObject *self, PyObject *a, PyObject *b, PyObject *c) +{ + Py_RETURN_NONE; +} + +static int +set_two(PyObject *self, PyObject *value) +{ + return 0; +} + +static PyObject *last_set_value = NULL; + +static int +set_three(PyObject *self, PyObject *value, void *closure) +{ + Py_XSETREF(last_set_value, Py_XNewRef(value)); + return 0; +} + +static PyObject * +get_last_set_value(PyObject *self, PyObject *Py_UNUSED(args)) +{ + if (last_set_value == NULL) { + Py_RETURN_NONE; + } + return Py_NewRef(last_set_value); +} + +#define FPCAST_METHODS(prefix) \ + {prefix "noargs0", _PyCFunction_CAST(zero), METH_NOARGS}, \ + {prefix "noargs1", _PyCFunction_CAST(one), METH_NOARGS}, \ + {prefix "noargs2", _PyCFunction_CAST(two), METH_NOARGS}, \ + {prefix "noargs3", _PyCFunction_CAST(three), METH_NOARGS}, \ + {prefix "noargs4", _PyCFunction_CAST(four), METH_NOARGS}, \ + \ + {prefix "o0", _PyCFunction_CAST(zero), METH_O}, \ + {prefix "o1", _PyCFunction_CAST(one), METH_O}, \ + {prefix "o2", _PyCFunction_CAST(two), METH_O}, \ + {prefix "o3", _PyCFunction_CAST(three), METH_O}, \ + \ + {prefix "varargs0", _PyCFunction_CAST(zero), METH_VARARGS}, \ + {prefix "varargs1", _PyCFunction_CAST(one), METH_VARARGS}, \ + {prefix "varargs2", _PyCFunction_CAST(two), METH_VARARGS}, \ + {prefix "varargs3", _PyCFunction_CAST(three), METH_VARARGS}, \ + \ + {prefix "kwargs0", _PyCFunction_CAST(zero), \ + METH_VARARGS | METH_KEYWORDS}, \ + {prefix "kwargs1", _PyCFunction_CAST(one), \ + METH_VARARGS | METH_KEYWORDS}, \ + {prefix "kwargs2", _PyCFunction_CAST(two), \ + METH_VARARGS | METH_KEYWORDS}, \ + {prefix "kwargs3", _PyCFunction_CAST(three), \ + METH_VARARGS | METH_KEYWORDS} + +static PyMethodDef test_methods[] = { + FPCAST_METHODS("fpcast_"), + {"fpcast_last_set_value", get_last_set_value, METH_NOARGS}, + {NULL}, +}; + +static PyMethodDef type_methods[] = { + FPCAST_METHODS(""), + {NULL}, +}; + +static PyGetSetDef type_getset[] = { + {"getset0", .get = _Py_FUNC_CAST(getter, zero)}, + {"getset1", .get = _Py_FUNC_CAST(getter, one), + .set = _Py_FUNC_CAST(setter, set_two)}, + {"getset2", .get = _Py_FUNC_CAST(getter, two), + .set = _Py_FUNC_CAST(setter, set_three)}, + {"getset4", .get = _Py_FUNC_CAST(getter, four), + .set = _Py_FUNC_CAST(setter, four)}, + {NULL}, +}; + +static PyTypeObject FpcastTestType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_testcapi.FpcastTestType", + .tp_basicsize = sizeof(PyObject), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = type_methods, + .tp_getset = type_getset, + .tp_new = PyType_GenericNew, +}; + +#define CALLABLE_TYPE(N, FUNC) \ + static PyTypeObject FpcastCallable##N = { \ + PyVarObject_HEAD_INIT(NULL, 0) \ + .tp_name = "_testcapi.FpcastCallable" #N, \ + .tp_basicsize = sizeof(PyObject), \ + .tp_flags = Py_TPFLAGS_DEFAULT, \ + .tp_call = _Py_FUNC_CAST(ternaryfunc, FUNC), \ + .tp_new = PyType_GenericNew, \ + }; + +CALLABLE_TYPE(0, zero) +CALLABLE_TYPE(1, one) +CALLABLE_TYPE(2, two) +CALLABLE_TYPE(3, three) +CALLABLE_TYPE(4, four) + +#undef CALLABLE_TYPE + +int +_PyTestCapi_Init_Fpcast(PyObject *mod) +{ + if (PyModule_AddFunctions(mod, test_methods) < 0) { + return -1; + } + PyTypeObject *types[] = { + &FpcastTestType, + &FpcastCallable0, + &FpcastCallable1, + &FpcastCallable2, + &FpcastCallable3, + &FpcastCallable4, + }; + for (size_t i = 0; i < Py_ARRAY_LENGTH(types); i++) { + if (PyModule_AddType(mod, types[i]) < 0) { + return -1; + } + } + return 0; +} diff --git a/Modules/_testcapi/parts.h b/Modules/_testcapi/parts.h index 1ae3f0773e42f80..e2787e7c93aef88 100644 --- a/Modules/_testcapi/parts.h +++ b/Modules/_testcapi/parts.h @@ -69,5 +69,6 @@ int _PyTestCapi_Init_Function(PyObject *mod); int _PyTestCapi_Init_Module(PyObject *mod); int _PyTestCapi_Init_Weakref(PyObject *mod); int _PyTestCapi_Init_Marshal(PyObject *mod); +int _PyTestCapi_Init_Fpcast(PyObject *mod); #endif // Py_TESTCAPI_PARTS_H diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 19c02e6774e3d48..033ee3234cf604d 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3832,6 +3832,9 @@ _testcapi_exec(PyObject *m) if (_PyTestCapi_Init_Marshal(m) < 0) { return -1; } + if (_PyTestCapi_Init_Fpcast(m) < 0) { + return -1; + } return 0; } diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 63023887667a50a..c0af1ed8cad8307 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -4,7 +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_wasm_trampoline.h" // descr_set_trampoline_call(), descr_get_trampoline_call() #include "pycore_descrobject.h" // _PyMethodWrapper_Type #include "pycore_modsupport.h" // _PyArg_UnpackStack() #include "pycore_object.h" // _PyObject_GC_UNTRACK() diff --git a/PCbuild/_testcapi.vcxproj b/PCbuild/_testcapi.vcxproj index d856b70bbdd5792..9b4473b877daf44 100644 --- a/PCbuild/_testcapi.vcxproj +++ b/PCbuild/_testcapi.vcxproj @@ -135,6 +135,7 @@ + diff --git a/PCbuild/_testcapi.vcxproj.filters b/PCbuild/_testcapi.vcxproj.filters index 554e5f3075f7ebf..238819a1d3646fb 100644 --- a/PCbuild/_testcapi.vcxproj.filters +++ b/PCbuild/_testcapi.vcxproj.filters @@ -138,6 +138,9 @@ Source Files + + Source Files + diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index fff7e3f9f8cdada..5a697bc4e9a4057 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -255,6 +255,7 @@ def configure_wasi_python(context, working_dir): f"--host={context.host_triple}", f"--build={context.build_python_path.name}", f"--with-build-python={build_python}", + "CFLAGS=-DPY_CALL_TRAMPOLINE", ] if context.is_debug: configure.append("--with-pydebug") diff --git a/Platforms/emscripten/prepare_external_wasm.py b/Platforms/emscripten/prepare_external_wasm.py deleted file mode 100644 index 1b0a9de4b1fe8d8..000000000000000 --- a/Platforms/emscripten/prepare_external_wasm.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import sys -from pathlib import Path - -JS_TEMPLATE = """ -#include "emscripten.h" - -EM_JS(void, {function_name}, (void), {{ - return new WebAssembly.Module(hexStringToUTF8Array("{hex_string}")); -}} -function hexStringToUTF8Array(hex) {{ - const bytes = []; - for (let i = 0; i < hex.length; i += 2) {{ - bytes.push(parseInt(hex.substr(i, 2), 16)); - }} - return new Uint8Array(bytes); -}}); -""" - - -def prepare_wasm(input_file, output_file, function_name): - # Read the compiled WASM as binary and convert to hex - wasm_bytes = Path(input_file).read_bytes() - - hex_string = "".join(f"{byte:02x}" for byte in wasm_bytes) - - # Generate JavaScript module - js_content = JS_TEMPLATE.format( - function_name=function_name, hex_string=hex_string - ) - Path(output_file).write_text(js_content) - - print(f"Successfully compiled {input_file} and generated {output_file}") - return 0 - - -def main(): - parser = argparse.ArgumentParser( - description="Compile WebAssembly text files using wasm-as" - ) - parser.add_argument("input_file", help="Input .wat file to compile") - parser.add_argument("output_file", help="Output file name") - parser.add_argument("function_name", help="Name of the export function") - - args = parser.parse_args() - - return prepare_wasm(args.input_file, args.output_file, args.function_name) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/Python/emscripten_trampoline.c b/Python/emscripten_trampoline.c deleted file mode 100644 index 547761027a3fb3d..000000000000000 --- a/Python/emscripten_trampoline.c +++ /dev/null @@ -1,136 +0,0 @@ -#if defined(PY_CALL_TRAMPOLINE) - -#include // EM_JS, EM_JS_DEPS -#include -#include "pycore_runtime.h" // _PyRuntime - -// We use the _PyRuntime.emscripten_trampoline field to store a function pointer -// for a wasm-gc based trampoline if it works. Otherwise fall back to JS -// trampoline. The JS trampoline breaks stack switching but every runtime that -// supports stack switching also supports wasm-gc. -// -// We'd like to make the trampoline call into a direct call but currently we -// need to import the wasmTable to compile trampolineModule. emcc >= 4.0.19 -// defines the table in WebAssembly and exports it so we won't have access to it -// until after the main module is compiled. -// -// To fix this, one natural solution would be to pass a funcref to the -// trampoline instead of a table index. Several PRs would be needed to fix -// things in llvm and emscripten in order to make this possible. -// -// The performance costs of an extra call_indirect aren't that large anyways. -// The JIT should notice that the target is always the same and turn into a -// check -// -// if (call_target != expected) deoptimize; -// direct_call(call_target, args); - -// Offset of emscripten_trampoline in _PyRuntimeState. There's a couple of -// alternatives: -// -// 1. Just make emscripten_trampoline a real C global variable instead of a -// field of _PyRuntimeState. This would violate our rule against mutable -// globals. -// -// 2. #define a preprocessor constant equal to a hard coded number and make a -// _Static_assert(offsetof(_PyRuntimeState, emscripten_trampoline) == OURCONSTANT) -// This has the disadvantage that we have to update the hard coded constant -// when _PyRuntimeState changes -// -// So putting the mutable constant in _PyRuntime and using a immutable global to -// record the offset so we can access it from JS is probably the best way. -EMSCRIPTEN_KEEPALIVE const int _PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET = offsetof(_PyRuntimeState, emscripten_trampoline); - -typedef PyObject* (*TrampolineFunc)(int* success, - PyCFunctionWithKeywords func, - PyObject* self, - PyObject* args, - PyObject* kw); - -// Lets JS reach _PyRuntime without it being in -sEXPORTED_FUNCTIONS. -EMSCRIPTEN_KEEPALIVE _PyRuntimeState *const _PyEM_runtime = &_PyRuntime; - -// Its table slot is taken over by the wasm-gc trampoline, so the table -// never grows. -static PyObject* -trampoline_placeholder(int* success, PyCFunctionWithKeywords func, - PyObject* self, PyObject* args, PyObject* kw) -{ - Py_FatalError("Emscripten trampoline slot was not set up"); -} -EMSCRIPTEN_KEEPALIVE const TrampolineFunc _PyEM_trampoline_slot = trampoline_placeholder; - -/** - * Backwards compatible trampoline works with all JS runtimes - */ -EM_JS(PyObject*, _PyEM_TrampolineCall_JS, (PyCFunctionWithKeywords func, PyObject *arg1, PyObject *arg2, PyObject *arg3), { - return wasmTable.get(func)(arg1, arg2, arg3); -} -// Try to compile wasm-gc trampoline if possible. -function getPyEMTrampolinePtr() { - // Starting with iOS 18.3.1, WebKit on iOS has an issue with the garbage - // collector that breaks the call trampoline. See #130418 and - // https://bugs.webkit.org/show_bug.cgi?id=293113 for details. - let isIOS = globalThis.navigator && ( - /iPad|iPhone|iPod/.test(navigator.userAgent) || - // Starting with iPadOS 13, iPads might send a platform string that looks like a desktop Mac. - // To differentiate, we check if the platform is 'MacIntel' (common for Macs and newer iPads) - // AND if the device has multi-touch capabilities (navigator.maxTouchPoints > 1) - (navigator.platform === 'MacIntel' && typeof navigator.maxTouchPoints !== 'undefined' && navigator.maxTouchPoints > 1) - ); - if (isIOS) { - return 0; - } - let trampolineModule; - try { - trampolineModule = getWasmTrampolineModule(); - } catch (e) { - // Compilation error due to missing wasm-gc support, fall back to JS - // trampoline - return 0; - } - const trampolineInstance = new WebAssembly.Instance(trampolineModule, { - env: { __indirect_function_table: wasmTable, memory: wasmMemory }, - }); - const slot = HEAPU32[__PyEM_trampoline_slot / 4]; - wasmTable.set(slot, trampolineInstance.exports.trampoline_call); - return slot; -} -// We have to be careful to work correctly with memory snapshots -- the value of -// _PyRuntimeState.emscripten_trampoline needs to reflect whether wasm-gc is -// available in the current runtime, not in the runtime the snapshot was taken -// in. This writes the appropriate value to -// _PyRuntimeState.emscripten_trampoline from JS startup code that runs every -// time, whether we are restoring a snapshot or not. -addOnPreRun(function setEmscriptenTrampoline() { - const ptr = getPyEMTrampolinePtr(); - const offset = HEAP32[__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET / 4]; - HEAP32[(HEAPU32[__PyEM_runtime / 4] + offset) / 4] = ptr; -}); -); - -EM_JS_DEPS(_PyEM_TrampolineCall, - "$wasmTable,$wasmMemory,$addOnPreRun"); - -PyObject* -_PyEM_TrampolineCall(PyCFunctionWithKeywords func, - PyObject* self, - PyObject* args, - PyObject* kw) -{ - TrampolineFunc trampoline = _PyRuntime.emscripten_trampoline; - if (trampoline == 0) { - return _PyEM_TrampolineCall_JS(func, self, args, kw); - } - int success = 1; - PyObject *result = trampoline(&success, func, self, args, kw); - if (!success) { - PyErr_SetString(PyExc_SystemError, "Handler takes too many arguments"); - } - return result; -} - -#else -// This is exported so we need to define it even when it isn't used -__attribute__((used)) const int _PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET = 0; -#endif diff --git a/Python/emscripten_trampoline_inner.c b/Python/emscripten_trampoline_inner.c deleted file mode 100644 index a2bad4857ed089c..000000000000000 --- a/Python/emscripten_trampoline_inner.c +++ /dev/null @@ -1,38 +0,0 @@ -// This file must be compiled with -mgc to enable the extra wasm-gc -// instructions. It has to be compiled separately because not enough JS runtimes -// support wasm-gc yet. If the JS runtime does not support wasm-gc (or has buggy -// support like iOS), we will use the JS trampoline fallback. - -// We can't import Python.h here because it is compiled/linked with -nostdlib. -// We don't need to know what's inside PyObject* anyways. We could just call it -// void* everywhere. There are two reasons to do this: -// 1. to improve readability -// 2. eventually when we are comfortable requiring wasm-gc, we can merge this -// into emscripten_trampoline.c without worrying about it. -typedef void PyObject; - -typedef PyObject* (*three_arg)(PyObject*, PyObject*, PyObject*); -typedef PyObject* (*two_arg)(PyObject*, PyObject*); -typedef PyObject* (*one_arg)(PyObject*); -typedef PyObject* (*zero_arg)(void); - -#define TRY_RETURN_CALL(ty, args...) \ - if (__builtin_wasm_test_function_pointer_signature((ty)func)) { \ - return ((ty)func)(args); \ - } - -__attribute__((export_name("trampoline_call"))) PyObject* -trampoline_call(int* success, - void* func, - PyObject* self, - PyObject* args, - PyObject* kw) -{ - *success = 1; - TRY_RETURN_CALL(three_arg, self, args, kw); - TRY_RETURN_CALL(two_arg, self, args); - TRY_RETURN_CALL(one_arg, self); - TRY_RETURN_CALL(zero_arg); - *success = 0; - return 0; -} diff --git a/Python/wasm_trampoline.c b/Python/wasm_trampoline.c new file mode 100644 index 000000000000000..e449ff72343e5c8 --- /dev/null +++ b/Python/wasm_trampoline.c @@ -0,0 +1,48 @@ +#if defined(__wasm__) && defined(PY_CALL_TRAMPOLINE) + +#include + +typedef PyObject* (*three_arg)(PyObject*, PyObject*, PyObject*); +typedef PyObject* (*two_arg)(PyObject*, PyObject*); +typedef PyObject* (*one_arg)(PyObject*); +typedef PyObject* (*zero_arg)(void); + +#define TRY_RETURN_CALL(ty, args...) \ + if (__builtin_wasm_test_function_pointer_signature((ty)func)) { \ + return ((ty)func)(args); \ + } + +PyObject* +_PyWasm_TrampolineCall(PyCFunctionWithKeywords func, + PyObject* self, + PyObject* args, + PyObject* kw) +{ + TRY_RETURN_CALL(three_arg, self, args, kw); + TRY_RETURN_CALL(two_arg, self, args); + TRY_RETURN_CALL(one_arg, self); + TRY_RETURN_CALL(zero_arg); + PyErr_SetString(PyExc_SystemError, "Handler has incorrect signature"); + return NULL; +} + +typedef int (*setter_three_arg)(PyObject*, PyObject*, void*); +typedef int (*setter_two_arg)(PyObject*, PyObject*); +typedef int (*setter_one_arg)(PyObject*); +typedef int (*setter_zero_arg)(void); + +int +_PyWasm_TrampolineCallSetter(setter func, + PyObject* self, + PyObject* val, + void* closure) +{ + TRY_RETURN_CALL(setter_three_arg, self, val, closure); + TRY_RETURN_CALL(setter_two_arg, self, val); + TRY_RETURN_CALL(setter_one_arg, self); + TRY_RETURN_CALL(setter_zero_arg); + PyErr_SetString(PyExc_SystemError, "Handler has incorrect signature"); + return -1; +} + +#endif // defined(__wasm__) && defined(PY_CALL_TRAMPOLINE) diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index 3c715f91cede109..caac194d3b7efb6 100644 --- a/Tools/c-analyzer/cpython/_parser.py +++ b/Tools/c-analyzer/cpython/_parser.py @@ -63,7 +63,6 @@ def format_tsv_lines(lines): 'Python/dynload_hpux.c', # dl.h 'Python/emscripten_signal.c', 'Python/emscripten_syscalls.c', - 'Python/emscripten_trampoline_inner.c', 'Python/thread_pthread.h', 'Python/thread_pthread_stubs.h', diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 2ad801c671855cf..615de640d6d8504 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -464,6 +464,13 @@ Modules/_testcapi/datetime.c - test_run_counter - Modules/_testcapi/docstring.c - DocStringNoSignatureTest - Modules/_testcapi/docstring.c - DocStringUnrepresentableSignatureTest - Modules/_testcapi/exceptions.c - PyRecursingInfinitelyError_Type - +Modules/_testcapi/fpcast.c - last_set_value - +Modules/_testcapi/fpcast.c - FpcastTestType - +Modules/_testcapi/fpcast.c - FpcastCallable0 - +Modules/_testcapi/fpcast.c - FpcastCallable1 - +Modules/_testcapi/fpcast.c - FpcastCallable2 - +Modules/_testcapi/fpcast.c - FpcastCallable3 - +Modules/_testcapi/fpcast.c - FpcastCallable4 - Modules/_testcapi/heaptype.c - _testcapimodule - Modules/_testcapi/mem.c - FmData - Modules/_testcapi/mem.c - FmHook - diff --git a/configure b/configure index 7d1003a0e0e985d..4e763f58ae581f7 100755 --- a/configure +++ b/configure @@ -9970,7 +9970,7 @@ fi as_fn_append LINKFORSHARED " -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js" as_fn_append LINKFORSHARED " -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY,ERRNO_CODES" - as_fn_append LINKFORSHARED " -sEXPORTED_FUNCTIONS=_main,_Py_Version,_PyGILState_GetThisThreadState,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET" + as_fn_append LINKFORSHARED " -sEXPORTED_FUNCTIONS=_main,_Py_Version,_PyGILState_GetThisThreadState" as_fn_append LINKFORSHARED " -sSTACK_SIZE=5MB" as_fn_append LINKFORSHARED " -sTEXTDECODER=2" @@ -20303,14 +20303,21 @@ PLATFORM_OBJS= case $ac_sys_system in #( Emscripten) : - as_fn_append PLATFORM_OBJS ' Python/emscripten_signal.o Python/emscripten_trampoline.o Python/emscripten_trampoline_wasm.o' + as_fn_append PLATFORM_OBJS ' Python/emscripten_signal.o Python/wasm_trampoline.o' + as_fn_append CFLAGS_NODIST " -mgc" + if test "x$enable_emscripten_syscalls" = xyes then : as_fn_append PLATFORM_OBJS ' Python/emscripten_syscalls.o' fi - as_fn_append PLATFORM_HEADERS ' $(srcdir)/Include/internal/pycore_emscripten_signal.h $(srcdir)/Include/internal/pycore_emscripten_trampoline.h' + as_fn_append PLATFORM_HEADERS ' $(srcdir)/Include/internal/pycore_emscripten_signal.h' + ;; #( + WASI) : + + as_fn_append PLATFORM_OBJS ' Python/wasm_trampoline.o' + as_fn_append CFLAGS_NODIST " -mgc" ;; #( *) : ;; diff --git a/configure.ac b/configure.ac index e1d55a7a3cef901..026cf15d9d8e622 100644 --- a/configure.ac +++ b/configure.ac @@ -2443,7 +2443,7 @@ AS_CASE([$ac_sys_system], dnl Include file system support AS_VAR_APPEND([LINKFORSHARED], [" -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"]) AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY,ERRNO_CODES"]) - AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,_PyGILState_GetThisThreadState,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET"]) + AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,_PyGILState_GetThisThreadState"]) AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"]) dnl Avoid bugs in JS fallback string decoding path AS_VAR_APPEND([LINKFORSHARED], [" -sTEXTDECODER=2"]) @@ -5443,11 +5443,17 @@ PLATFORM_OBJS= AS_CASE([$ac_sys_system], [Emscripten], [ - AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o Python/emscripten_trampoline.o Python/emscripten_trampoline_wasm.o']) + AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o Python/wasm_trampoline.o']) + AS_VAR_APPEND([CFLAGS_NODIST], [" -mgc"]) + AS_VAR_IF([enable_emscripten_syscalls], [yes], [ AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_syscalls.o']) ]) - AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h $(srcdir)/Include/internal/pycore_emscripten_trampoline.h']) + AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h']) + ], + [WASI], [ + AS_VAR_APPEND([PLATFORM_OBJS], [' Python/wasm_trampoline.o']) + AS_VAR_APPEND([CFLAGS_NODIST], [" -mgc"]) ], ) AC_SUBST([PLATFORM_HEADERS])