summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPeter Bierma <zintensitydev@gmail.com>2024-12-11 11:14:04 (GMT)
committerGitHub <noreply@github.com>2024-12-11 11:14:04 (GMT)
commitd5d84c3f13fe7fe591b375c41979d362bc11957a (patch)
treef5256d76f43516d757ab68ff64eeb05851644969
parent2cdeb61b57e638ae46a04386330a12abe9cddf2c (diff)
downloadcpython-d5d84c3f13fe7fe591b375c41979d362bc11957a.zip
cpython-d5d84c3f13fe7fe591b375c41979d362bc11957a.tar.gz
cpython-d5d84c3f13fe7fe591b375c41979d362bc11957a.tar.bz2
gh-127791: Fix, document, and test `PyUnstable_AtExit` (#127793)
-rw-r--r--Doc/c-api/init.rst9
-rw-r--r--Doc/c-api/sys.rst4
-rw-r--r--Include/internal/pycore_atexit.h1
-rw-r--r--Misc/NEWS.d/next/C_API/2024-12-10-14-25-22.gh-issue-127791.YRw4GU.rst2
-rw-r--r--Modules/_testcapimodule.c48
-rw-r--r--Modules/_testinternalcapi.c34
-rw-r--r--Modules/atexitmodule.c12
7 files changed, 71 insertions, 39 deletions
diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst
index ba1c285..dd63dd0 100644
--- a/Doc/c-api/init.rst
+++ b/Doc/c-api/init.rst
@@ -567,6 +567,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
=======================
diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst
index d6fca1a..c688afd 100644
--- a/Doc/c-api/sys.rst
+++ b/Doc/c-api/sys.rst
@@ -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.
diff --git a/Include/internal/pycore_atexit.h b/Include/internal/pycore_atexit.h
index 507a5c0..cde5b53 100644
--- a/Include/internal/pycore_atexit.h
+++ b/Include/internal/pycore_atexit.h
@@ -44,7 +44,6 @@ typedef struct {
struct atexit_state {
atexit_callback *ll_callbacks;
- atexit_callback *last_ll_callback;
// XXX The rest of the state could be moved to the atexit module state
// and a low-level callback added for it during module exec.
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
index 0000000..70751f1
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2024-12-10-14-25-22.gh-issue-127791.YRw4GU.rst
@@ -0,0 +1,2 @@
+Fix loss of callbacks after more than one call to
+:c:func:`PyUnstable_AtExit`.
diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c
index 26f6869..8d86b53 100644
--- a/Modules/_testcapimodule.c
+++ b/Modules/_testcapimodule.c
@@ -3353,6 +3353,53 @@ type_freeze(PyObject *module, PyObject *args)
Py_RETURN_NONE;
}
+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},
@@ -3495,6 +3542,7 @@ static PyMethodDef TestMethods[] = {
{"test_critical_sections", test_critical_sections, METH_NOARGS},
{"finalize_thread_hang", finalize_thread_hang, METH_O, NULL},
{"type_freeze", type_freeze, METH_VARARGS},
+ {"test_atexit", test_atexit, METH_NOARGS},
{NULL, NULL} /* sentinel */
};
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index 1bb71a3..288daf0 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -1236,39 +1236,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)
{
@@ -2128,7 +2095,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},
diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c
index 297a8d7..c009235 100644
--- a/Modules/atexitmodule.c
+++ b/Modules/atexitmodule.c
@@ -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;
}