Skip to content

Commit d5d84c3

Browse files
gh-127791: Fix, document, and test PyUnstable_AtExit (#127793)
1 parent 2cdeb61 commit d5d84c3

File tree

7 files changed

+71
-39
lines changed

7 files changed

+71
-39
lines changed

Doc/c-api/init.rst

+9
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,15 @@ Initializing and finalizing the interpreter
567567
customized Python that always runs in isolated mode using
568568
:c:func:`Py_RunMain`.
569569
570+
.. c:function:: int PyUnstable_AtExit(PyInterpreterState *interp, void (*func)(void *), void *data)
571+
572+
Register an :mod:`atexit` callback for the target interpreter *interp*.
573+
This is similar to :c:func:`Py_AtExit`, but takes an explicit interpreter and
574+
data pointer for the callback.
575+
576+
The :term:`GIL` must be held for *interp*.
577+
578+
.. versionadded:: 3.13
570579
571580
Process-wide parameters
572581
=======================

Doc/c-api/sys.rst

+4
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,7 @@ Process Control
426426
function registered last is called first. Each cleanup function will be called
427427
at most once. Since Python's internal finalization will have completed before
428428
the cleanup function, no Python APIs should be called by *func*.
429+
430+
.. seealso::
431+
432+
:c:func:`PyUnstable_AtExit` for passing a ``void *data`` argument.

Include/internal/pycore_atexit.h

-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ typedef struct {
4444

4545
struct atexit_state {
4646
atexit_callback *ll_callbacks;
47-
atexit_callback *last_ll_callback;
4847

4948
// XXX The rest of the state could be moved to the atexit module state
5049
// and a low-level callback added for it during module exec.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix loss of callbacks after more than one call to
2+
:c:func:`PyUnstable_AtExit`.

Modules/_testcapimodule.c

+48
Original file line numberDiff line numberDiff line change
@@ -3353,6 +3353,53 @@ type_freeze(PyObject *module, PyObject *args)
33533353
Py_RETURN_NONE;
33543354
}
33553355

3356+
struct atexit_data {
3357+
int called;
3358+
PyThreadState *tstate;
3359+
PyInterpreterState *interp;
3360+
};
3361+
3362+
static void
3363+
atexit_callback(void *data)
3364+
{
3365+
struct atexit_data *at_data = (struct atexit_data *)data;
3366+
// Ensure that the callback is from the same interpreter
3367+
assert(PyThreadState_Get() == at_data->tstate);
3368+
assert(PyInterpreterState_Get() == at_data->interp);
3369+
++at_data->called;
3370+
}
3371+
3372+
static PyObject *
3373+
test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
3374+
{
3375+
PyThreadState *oldts = PyThreadState_Swap(NULL);
3376+
PyThreadState *tstate = Py_NewInterpreter();
3377+
3378+
struct atexit_data data = {0};
3379+
data.tstate = PyThreadState_Get();
3380+
data.interp = PyInterpreterState_Get();
3381+
3382+
int amount = 10;
3383+
for (int i = 0; i < amount; ++i)
3384+
{
3385+
int res = PyUnstable_AtExit(tstate->interp, atexit_callback, (void *)&data);
3386+
if (res < 0) {
3387+
Py_EndInterpreter(tstate);
3388+
PyThreadState_Swap(oldts);
3389+
PyErr_SetString(PyExc_RuntimeError, "atexit callback failed");
3390+
return NULL;
3391+
}
3392+
}
3393+
3394+
Py_EndInterpreter(tstate);
3395+
PyThreadState_Swap(oldts);
3396+
3397+
if (data.called != amount) {
3398+
PyErr_SetString(PyExc_RuntimeError, "atexit callback not called");
3399+
return NULL;
3400+
}
3401+
Py_RETURN_NONE;
3402+
}
33563403

33573404
static PyMethodDef TestMethods[] = {
33583405
{"set_errno", set_errno, METH_VARARGS},
@@ -3495,6 +3542,7 @@ static PyMethodDef TestMethods[] = {
34953542
{"test_critical_sections", test_critical_sections, METH_NOARGS},
34963543
{"finalize_thread_hang", finalize_thread_hang, METH_O, NULL},
34973544
{"type_freeze", type_freeze, METH_VARARGS},
3545+
{"test_atexit", test_atexit, METH_NOARGS},
34983546
{NULL, NULL} /* sentinel */
34993547
};
35003548

Modules/_testinternalcapi.c

-34
Original file line numberDiff line numberDiff line change
@@ -1236,39 +1236,6 @@ unicode_transformdecimalandspacetoascii(PyObject *self, PyObject *arg)
12361236
return _PyUnicode_TransformDecimalAndSpaceToASCII(arg);
12371237
}
12381238

1239-
1240-
struct atexit_data {
1241-
int called;
1242-
};
1243-
1244-
static void
1245-
callback(void *data)
1246-
{
1247-
((struct atexit_data *)data)->called += 1;
1248-
}
1249-
1250-
static PyObject *
1251-
test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
1252-
{
1253-
PyThreadState *oldts = PyThreadState_Swap(NULL);
1254-
PyThreadState *tstate = Py_NewInterpreter();
1255-
1256-
struct atexit_data data = {0};
1257-
int res = PyUnstable_AtExit(tstate->interp, callback, (void *)&data);
1258-
Py_EndInterpreter(tstate);
1259-
PyThreadState_Swap(oldts);
1260-
if (res < 0) {
1261-
return NULL;
1262-
}
1263-
1264-
if (data.called == 0) {
1265-
PyErr_SetString(PyExc_RuntimeError, "atexit callback not called");
1266-
return NULL;
1267-
}
1268-
Py_RETURN_NONE;
1269-
}
1270-
1271-
12721239
static PyObject *
12731240
test_pyobject_is_freed(const char *test_name, PyObject *op)
12741241
{
@@ -2128,7 +2095,6 @@ static PyMethodDef module_functions[] = {
21282095
{"_PyTraceMalloc_GetTraceback", tracemalloc_get_traceback, METH_VARARGS},
21292096
{"test_tstate_capi", test_tstate_capi, METH_NOARGS, NULL},
21302097
{"_PyUnicode_TransformDecimalAndSpaceToASCII", unicode_transformdecimalandspacetoascii, METH_O},
2131-
{"test_atexit", test_atexit, METH_NOARGS},
21322098
{"check_pyobject_forbidden_bytes_is_freed",
21332099
check_pyobject_forbidden_bytes_is_freed, METH_NOARGS},
21342100
{"check_pyobject_freed_is_freed", check_pyobject_freed_is_freed, METH_NOARGS},

Modules/atexitmodule.c

+8-4
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ int
2727
PyUnstable_AtExit(PyInterpreterState *interp,
2828
atexit_datacallbackfunc func, void *data)
2929
{
30-
assert(interp == _PyInterpreterState_GET());
30+
PyThreadState *tstate = _PyThreadState_GET();
31+
_Py_EnsureTstateNotNULL(tstate);
32+
assert(tstate->interp == interp);
33+
3134
atexit_callback *callback = PyMem_Malloc(sizeof(atexit_callback));
3235
if (callback == NULL) {
3336
PyErr_NoMemory();
@@ -38,12 +41,13 @@ PyUnstable_AtExit(PyInterpreterState *interp,
3841
callback->next = NULL;
3942

4043
struct atexit_state *state = &interp->atexit;
41-
if (state->ll_callbacks == NULL) {
44+
atexit_callback *top = state->ll_callbacks;
45+
if (top == NULL) {
4246
state->ll_callbacks = callback;
43-
state->last_ll_callback = callback;
4447
}
4548
else {
46-
state->last_ll_callback->next = callback;
49+
callback->next = top;
50+
state->ll_callbacks = callback;
4751
}
4852
return 0;
4953
}

0 commit comments

Comments
 (0)