Skip to content

Commit 27dcd28

Browse files
committed
Fix asyncio loop lookup on Python 3.14.0–3.14.3 (#515)
Python 3.14.4 appended `datastack_cached_chunk` to the public `PyThreadState` struct (python/cpython@19cbcc0f85, gh-142183), making `sizeof(PyThreadState)` 8 bytes larger. `_PyThreadStateImpl` embeds `PyThreadState` by value, so every private field after it, including `asyncio_running_loop`, shifted by 8 bytes. The offset was fixed at compile time by the headers the wheel was built against. A wheel built with 3.14.4+ headers and run on 3.14.0–3.14.3 read `asyncio_running_task` instead, which treated the running Task as the event loop and crashed on any `await` of a JS promise. A wheel built with older headers and run on 3.14.4+ read `c_stack_hard_limit` and crashed the same way. On Python 3.14, compare the runtime `Py_Version` with the compile-time `PY_VERSION_HEX`, and move the offset by one pointer when they fall on different sides of 3.14.4.
1 parent 8789e60 commit 27dcd28

1 file changed

Lines changed: 21 additions & 1 deletion

File tree

‎src/PyEventLoop.cc‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
#include <Python.h>
1515
#include <stdint.h>
16+
#include <stddef.h>
1617

1718
/**
1819
* @brief Wrapper to decrement the counter of queueing event-loop jobs after the job finishes
@@ -205,7 +206,26 @@ PyEventLoop PyEventLoop::_getLoopOnThread(PyThreadState *tstate) {
205206
};
206207

207208
// Modified from https://github.com/python/cpython/blob/v3.13.0rc1/Modules/_asynciomodule.c#L3205-L3210
208-
PyObject *loop = ((PyThreadStateHolder *)tstate)->asyncio_running_loop;
209+
size_t loopOffset = offsetof(PyThreadStateHolder, asyncio_running_loop);
210+
#if PY_VERSION_HEX >= 0x030e0000 && PY_VERSION_HEX < 0x030f0000 // Python 3.14
211+
// Python 3.14.4 appended a pointer field (`datastack_cached_chunk`) to the public `PyThreadState` struct,
212+
// so `sizeof(PyThreadState)` is 8 bytes larger, and every `_PyThreadStateImpl` field after it is shifted.
213+
// see https://github.com/python/cpython/commit/19cbcc0f85 (gh-142183)
214+
// https://github.com/Distributive-Network/PythonMonkey/issues/515
215+
// `sizeof(PyThreadState)` is fixed at compile time by the headers we build against,
216+
// so adjust the offset if the running interpreter is on the other side of 3.14.4.
217+
{
218+
constexpr unsigned long tstateGrewVersion = 0x030e0400; // 3.14.4
219+
bool builtWithCachedChunk = PY_VERSION_HEX >= tstateGrewVersion;
220+
bool runningWithCachedChunk = Py_Version >= tstateGrewVersion;
221+
if (builtWithCachedChunk && !runningWithCachedChunk) {
222+
loopOffset -= sizeof(void *);
223+
} else if (!builtWithCachedChunk && runningWithCachedChunk) {
224+
loopOffset += sizeof(void *);
225+
}
226+
}
227+
#endif // Python 3.14
228+
PyObject *loop = *(PyObject **)((char *)tstate + loopOffset); // = ((PyThreadStateHolder *)tstate)->asyncio_running_loop;
209229
if (loop == NULL) {
210230
return _loopNotFound();
211231
}

0 commit comments

Comments
 (0)