summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2022-10-28 15:18:37 -0500
committerJason Madden <jamadden@gmail.com>2022-10-28 15:18:37 -0500
commit8ab940786f29d01dbcc7df2ef24940d27389e7a9 (patch)
tree89365d625617d4ca33df3e4405a312a1cbd99d0b /src
parentfb2511380bb13dc5033c8fd9e303cf97a9cf3431 (diff)
downloadgreenlet-8ab940786f29d01dbcc7df2ef24940d27389e7a9.tar.gz
We cannot reliably include internal/pycore_pystate.h, so copy the needed definitions.
Diffstat (limited to 'src')
-rw-r--r--src/greenlet/greenlet.cpp167
1 files changed, 150 insertions, 17 deletions
diff --git a/src/greenlet/greenlet.cpp b/src/greenlet/greenlet.cpp
index 1f64997..64d7db4 100644
--- a/src/greenlet/greenlet.cpp
+++ b/src/greenlet/greenlet.cpp
@@ -406,35 +406,165 @@ struct ThreadState_DestroyWithGIL
#if PY_VERSION_HEX >= 0x30800A0 && PY_VERSION_HEX < 0x3090000
// XXX: From Python 3.8a3 [1] up until Python 3.9a6 [2][3],
-// Py_AddPendingCall would try to produce a Python exception if the
-// interpreter was in the middle of shutting down when this function
-// is called. However, this function doesn't require the GIL, and we
-// are absolutely not holding it. That means that trying to create the
-// Python exception is using the C API in an undefined state; under
-// some circumstances at least, the C API detects this and aborts the
-// process with an error ("Fatal Python error: Python memory allocator
-// called without holding the GIL": Add -> PyErr_SetString ->
-// PyUnicode_New -> PyObject_Malloc)
+// ``Py_AddPendingCall`` would try to produce a Python exception if
+// the interpreter was in the beginning of shutting down when this
+// function is called. However, ``Py_AddPendingCall`` doesn't require
+// the GIL, and we are absolutely not holding it when we make that
+// call. That means that trying to create the Python exception is
+// using the C API in an undefined state; here the C API detects this
+// and aborts the process with an error ("Fatal Python error: Python
+// memory allocator called without holding the GIL": Add ->
+// PyErr_SetString -> PyUnicode_New -> PyObject_Malloc). This arises
+// (obviously) in multi-threaded programs and happens if one thread is
+// exiting and cleaning up its thread-local data while the other
+// thread is trying to shut down the interpreter. A crash on shutdown
+// is still a crash and could result in data loss (e.g., daemon
+// threads are still running, pending signal handlers may be present,
+// buffers may not be flushed, there may be __del__ that need run,
+// etc), so we have to work around it.
//
-// Of course, we checked for whether the interpreter was shutting down
-// earlier, but as noted, that's a race condition, and so we may not
-// have actually gotten the right answer.
+// Of course, we can (and do) check for whether the interpreter is
+// shutting down before calling ``Py_AddPendingCall``, but that's a
+// race condition since we don't hold the GIL, and so we may not
+// actually get the right answer. Plus, ``Py_FinalizeEx`` actually
+// calls ``_Py_FinishPendingCalls`` (which sets the pending->finishing
+// flag, which is used to gate creating the exceptioen) *before*
+// publishing any other data that would let us detect the shutdown
+// (such as runtime->finalizing). So that point is moot.
//
// Our solution for those versions is to inline the same code, without
-// the problematic bit.
+// the problematic bit that sets the exception. Unfortunately, all of
+// the structure definitions are private/opaque, *and* we can't
+// actually count on being able to include their definitions from
+// ``internal/pycore_*``, because on some platforms those header files
+// are incomplete (i.e., on macOS with macports 3.8, the includes are
+// fine, but on Ubuntu jammy with 3.8 from ppa:deadsnakes or GitHub
+// Actions 3.8 (I think it's Ubuntu 18.04), they con't be used; at
+// least, I couldn't get them to work). So we need to define the
+// structures and _PyRuntime data member ourself.
//
// [1] https://github.com/python/cpython/commit/842a2f07f2f08a935ef470bfdaeef40f87490cfc
// [2] https://github.com/python/cpython/commit/cfc3c2f8b34d3864717ab584c5b6c260014ba55a
// [3] https://github.com/python/cpython/issues/81308
# define GREENLET_BROKEN_PY_ADD_PENDING 1
-# define Py_BUILD_CORE 1
-# include "internal/pycore_pystate.h"
-# undef Py_BUILD_CORE
+
+// When defining these structures, the important thing is to get
+// binary compatibility, i.e., structure layout. For that, we only
+// need to define fields up to the ones we use; after that they're
+// irrelevant UNLESS the structure is included in another structure
+// *before* the structure we're interested in --- in that case, it
+// must be complete. Ellipsis indicate elided trailing members.
+// Pointer types are changed to void* to keep from having to define
+// more structures.
+
+// From "internal/pycore_atomic.h"
+
+// There are several different definitions of this, including the
+// plain ``int`` version, a ``volatile int`` and an ``_Atomic int``
+// I don't think any of those change the size/layout.
+typedef struct _Py_atomic_int {
+ volatile int _value;
+} _Py_atomic_int;
+
+// This needs too much infrastructure, so we just do a regular store.
+#define _Py_atomic_store_relaxed(ATOMIC_VAL, NEW_VAL) \
+ (ATOMIC_VAL)->_value = NEW_VAL
+
+
+
+// From "internal/pycore_pymem.h"
+#define NUM_GENERATIONS 3
+
+
+struct gc_generation {
+ PyGC_Head head; // We already have this defined.
+ int threshold;
+ int count;
+};
+struct gc_generation_stats {
+ Py_ssize_t collections;
+ Py_ssize_t collected;
+ Py_ssize_t uncollectable;
+};
+
+struct _gc_runtime_state {
+ void *trash_delete_later;
+ int trash_delete_nesting;
+ int enabled;
+ int debug;
+ struct gc_generation generations[NUM_GENERATIONS];
+ void *generation0;
+ struct gc_generation permanent_generation;
+ struct gc_generation_stats generation_stats[NUM_GENERATIONS];
+ int collecting;
+ void *garbage;
+ void *callbacks;
+ Py_ssize_t long_lived_total;
+ Py_ssize_t long_lived_pending;
+};
+
+// From "internal/pycore_pystate.h"
+struct _pending_calls {
+ int finishing;
+ PyThread_type_lock lock;
+ _Py_atomic_int calls_to_do;
+ int async_exc;
+#define NPENDINGCALLS 32
+ struct {
+ int (*func)(void *);
+ void *arg;
+ } calls[NPENDINGCALLS];
+ int first;
+ int last;
+};
+
+struct _ceval_runtime_state {
+ int recursion_limit;
+ int tracing_possible;
+ _Py_atomic_int eval_breaker;
+ _Py_atomic_int gil_drop_request;
+ struct _pending_calls pending;
+ // ...
+};
+
+typedef struct pyruntimestate {
+ int preinitializing;
+ int preinitialized;
+ int core_initialized;
+ int initialized;
+ void *finalizing;
+
+ struct pyinterpreters {
+ PyThread_type_lock mutex;
+ void *head;
+ void *main;
+ int64_t next_id;
+ } interpreters;
+ // XXX Remove this field once we have a tp_* slot.
+ struct _xidregistry {
+ PyThread_type_lock mutex;
+ void *head;
+ } xidregistry;
+
+ unsigned long main_thread;
+
+#define NEXITFUNCS 32
+ void (*exitfuncs[NEXITFUNCS])(void);
+ int nexitfuncs;
+
+ struct _gc_runtime_state gc;
+ struct _ceval_runtime_state ceval;
+ // ...
+} _PyRuntimeState;
+
#define SIGNAL_PENDING_CALLS(ceval) \
do { \
_Py_atomic_store_relaxed(&(ceval)->pending.calls_to_do, 1); \
_Py_atomic_store_relaxed(&(ceval)->eval_breaker, 1); \
} while (0)
+
+extern _PyRuntimeState _PyRuntime;
+
#else
# define GREENLET_BROKEN_PY_ADD_PENDING 0
#endif
@@ -456,10 +586,12 @@ struct ThreadState_DestroyNoGIL
pending->last = j;
return 0;
}
+
static int AddPendingCall(int (*func)(void *), void *arg)
{
_PyRuntimeState *runtime = &_PyRuntime;
if (!runtime) {
+ // obviously impossible
return 0;
}
struct _pending_calls *pending = &runtime->ceval.pending;
@@ -469,13 +601,14 @@ struct ThreadState_DestroyNoGIL
int result = 0;
PyThread_acquire_lock(pending->lock, WAIT_LOCK);
if (!pending->finishing) {
- result = ThreadState_DestroyNoGIL::_push_pending_call(pending, func, arg);
+ result = _push_pending_call(pending, func, arg);
}
PyThread_release_lock(pending->lock);
SIGNAL_PENDING_CALLS(&runtime->ceval);
return result;
}
#else
+ // Python < 3.8 or >= 3.9
static int AddPendingCall(int (*func)(void*), void* arg)
{
return Py_AddPendingCall(func, arg);