]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-127791: Fix, document, and test `PyUnstable_AtExit` (GH-127793) (#127819)
authorPeter Bierma <zintensitydev@gmail.com>
Wed, 11 Dec 2024 13:40:45 +0000 (08:40 -0500)
committerGitHub <noreply@github.com>
Wed, 11 Dec 2024 13:40:45 +0000 (13:40 +0000)
* Fix merge conflicts.

* [3.13] gh-127791: Fix, document, and test `PyUnstable_AtExit` (GH-127793)
(cherry picked from commit d5d84c3f13fe7fe591b375c41979d362bc11957a)

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
Doc/c-api/init.rst
Doc/c-api/sys.rst
Include/internal/pycore_atexit.h
Misc/NEWS.d/next/C API/2024-12-10-14-25-22.gh-issue-127791.YRw4GU.rst [new file with mode: 0644]
Modules/_testcapimodule.c
Modules/_testinternalcapi.c
Modules/atexitmodule.c

index 385bed49511f605da58f13e445a154f388a10f5c..d0632cb6fa172f768ee1ded7dab7d80e18907d3a 100644 (file)
@@ -557,6 +557,15 @@ Initializing and finalizing the interpreter
    customized Python that always runs in isolated mode using
    :c:func:`Py_RunMain`.
 
+.. c:function:: int PyUnstable_AtExit(PyInterpreterState *interp, void (*func)(void *), void *data)
+
+   Register an :mod:`atexit` callback for the target interpreter *interp*.
+   This is similar to :c:func:`Py_AtExit`, but takes an explicit interpreter and
+   data pointer for the callback.
+
+   The :term:`GIL` must be held for *interp*.
+
+   .. versionadded:: 3.13
 
 Process-wide parameters
 =======================
index d6fca1a0b0a219cbfa3a5337f01704cdf9f21461..c688afdca8231d32bcbd120467306ed8bf07921a 100644 (file)
@@ -426,3 +426,7 @@ Process Control
    function registered last is called first. Each cleanup function will be called
    at most once.  Since Python's internal finalization will have completed before
    the cleanup function, no Python APIs should be called by *func*.
+
+   .. seealso::
+
+      :c:func:`PyUnstable_AtExit` for passing a ``void *data`` argument.
index 507a5c03cbc792904a6a1b8cd95f9366022acbdd..72c66a059395009b6985158b42221ef9c7774d5c 100644 (file)
@@ -44,6 +44,7 @@ typedef struct {
 
 struct atexit_state {
     atexit_callback *ll_callbacks;
+    // Kept for ABI compatibility--do not use! (See GH-127791.)
     atexit_callback *last_ll_callback;
 
     // XXX The rest of the state could be moved to the atexit module state
diff --git a/Misc/NEWS.d/next/C API/2024-12-10-14-25-22.gh-issue-127791.YRw4GU.rst b/Misc/NEWS.d/next/C API/2024-12-10-14-25-22.gh-issue-127791.YRw4GU.rst
new file mode 100644 (file)
index 0000000..70751f1
--- /dev/null
@@ -0,0 +1,2 @@
+Fix loss of callbacks after more than one call to
+:c:func:`PyUnstable_AtExit`.
index 01b6bd89d1371e4ee2c52b6ed5a16f01aa2814c0..b8c13c63f9515320df81044ba1716629804575e3 100644 (file)
@@ -3338,6 +3338,54 @@ pyeval_getlocals(PyObject *module, PyObject *Py_UNUSED(args))
     return Py_XNewRef(PyEval_GetLocals());
 }
 
+struct atexit_data {
+    int called;
+    PyThreadState *tstate;
+    PyInterpreterState *interp;
+};
+
+static void
+atexit_callback(void *data)
+{
+    struct atexit_data *at_data = (struct atexit_data *)data;
+    // Ensure that the callback is from the same interpreter
+    assert(PyThreadState_Get() == at_data->tstate);
+    assert(PyInterpreterState_Get() == at_data->interp);
+    ++at_data->called;
+}
+
+static PyObject *
+test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
+{
+    PyThreadState *oldts = PyThreadState_Swap(NULL);
+    PyThreadState *tstate = Py_NewInterpreter();
+
+    struct atexit_data data = {0};
+    data.tstate = PyThreadState_Get();
+    data.interp = PyInterpreterState_Get();
+
+    int amount = 10;
+    for (int i = 0; i < amount; ++i)
+    {
+        int res = PyUnstable_AtExit(tstate->interp, atexit_callback, (void *)&data);
+        if (res < 0) {
+            Py_EndInterpreter(tstate);
+            PyThreadState_Swap(oldts);
+            PyErr_SetString(PyExc_RuntimeError, "atexit callback failed");
+            return NULL;
+        }
+    }
+
+    Py_EndInterpreter(tstate);
+    PyThreadState_Swap(oldts);
+
+    if (data.called != amount) {
+        PyErr_SetString(PyExc_RuntimeError, "atexit callback not called");
+        return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
 static PyMethodDef TestMethods[] = {
     {"set_errno",               set_errno,                       METH_VARARGS},
     {"test_config",             test_config,                     METH_NOARGS},
@@ -3483,6 +3531,7 @@ static PyMethodDef TestMethods[] = {
     {"function_set_warning", function_set_warning, METH_NOARGS},
     {"test_critical_sections", test_critical_sections, METH_NOARGS},
     {"pyeval_getlocals", pyeval_getlocals, METH_NOARGS},
+    {"test_atexit", test_atexit, METH_NOARGS},
     {NULL, NULL} /* sentinel */
 };
 
index 6185fa313daa096da0b518987a7672e8323bf83a..dd0fe61d42d25e1483a235fa97cbcfb8253ecaa5 100644 (file)
@@ -1234,39 +1234,6 @@ unicode_transformdecimalandspacetoascii(PyObject *self, PyObject *arg)
     return _PyUnicode_TransformDecimalAndSpaceToASCII(arg);
 }
 
-
-struct atexit_data {
-    int called;
-};
-
-static void
-callback(void *data)
-{
-    ((struct atexit_data *)data)->called += 1;
-}
-
-static PyObject *
-test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
-{
-    PyThreadState *oldts = PyThreadState_Swap(NULL);
-    PyThreadState *tstate = Py_NewInterpreter();
-
-    struct atexit_data data = {0};
-    int res = PyUnstable_AtExit(tstate->interp, callback, (void *)&data);
-    Py_EndInterpreter(tstate);
-    PyThreadState_Swap(oldts);
-    if (res < 0) {
-        return NULL;
-    }
-
-    if (data.called == 0) {
-        PyErr_SetString(PyExc_RuntimeError, "atexit callback not called");
-        return NULL;
-    }
-    Py_RETURN_NONE;
-}
-
-
 static PyObject *
 test_pyobject_is_freed(const char *test_name, PyObject *op)
 {
@@ -2065,7 +2032,6 @@ static PyMethodDef module_functions[] = {
     {"_PyTraceMalloc_GetTraceback", tracemalloc_get_traceback, METH_VARARGS},
     {"test_tstate_capi", test_tstate_capi, METH_NOARGS, NULL},
     {"_PyUnicode_TransformDecimalAndSpaceToASCII", unicode_transformdecimalandspacetoascii, METH_O},
-    {"test_atexit", test_atexit, METH_NOARGS},
     {"check_pyobject_forbidden_bytes_is_freed",
                             check_pyobject_forbidden_bytes_is_freed, METH_NOARGS},
     {"check_pyobject_freed_is_freed", check_pyobject_freed_is_freed, METH_NOARGS},
index 297a8d74ba3bf4c1f8f84f8f4b9a6b3a80e305b8..c009235b7a36c20b62b79f5ef280210f0866784e 100644 (file)
@@ -27,7 +27,10 @@ int
 PyUnstable_AtExit(PyInterpreterState *interp,
                   atexit_datacallbackfunc func, void *data)
 {
-    assert(interp == _PyInterpreterState_GET());
+    PyThreadState *tstate = _PyThreadState_GET();
+    _Py_EnsureTstateNotNULL(tstate);
+    assert(tstate->interp == interp);
+
     atexit_callback *callback = PyMem_Malloc(sizeof(atexit_callback));
     if (callback == NULL) {
         PyErr_NoMemory();
@@ -38,12 +41,13 @@ PyUnstable_AtExit(PyInterpreterState *interp,
     callback->next = NULL;
 
     struct atexit_state *state = &interp->atexit;
-    if (state->ll_callbacks == NULL) {
+    atexit_callback *top = state->ll_callbacks;
+    if (top == NULL) {
         state->ll_callbacks = callback;
-        state->last_ll_callback = callback;
     }
     else {
-        state->last_ll_callback->next = callback;
+        callback->next = top;
+        state->ll_callbacks = callback;
     }
     return 0;
 }