diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h
index 3ecc5e9f0554b0..dbc0ebf994fd1e 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 145e66de9984ca..5470f5858eda08 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 e37c53a64f4a72..1199bab0aca097 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 00000000000000..ae19ac11f6a191
--- /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 42480f28ad7f31..36515dbd146a1e 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 00000000000000..50f432d71e22f3
--- /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 dd9f38e59f7a3f..d8cd8de10dfe08 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 00000000000000..ed6e2d3c5c5e6d
--- /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 1ae3f0773e42f8..e2787e7c93aef8 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 19c02e6774e3d4..033ee3234cf604 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 63023887667a50..c0af1ed8cad830 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 d856b70bbdd579..9b4473b877daf4 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 554e5f3075f7eb..238819a1d3646f 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 fff7e3f9f8cdad..5a697bc4e9a405 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 1b0a9de4b1fe8d..00000000000000
--- 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 547761027a3fb3..00000000000000
--- 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 a2bad4857ed089..00000000000000
--- 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 00000000000000..e449ff72343e5c
--- /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 3c715f91cede10..caac194d3b7efb 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 2ad801c671855c..615de640d6d850 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 7d1003a0e0e985..4e763f58ae581f 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 e1d55a7a3cef90..026cf15d9d8e62 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])