Skip to content

Commit bcdd10d

Browse files
[3.13] gh-127791: Fix, document, and test PyUnstable_AtExit (GH-127793) (#127819)
* Fix merge conflicts. * [3.13] gh-127791: Fix, document, and test `PyUnstable_AtExit` (GH-127793) (cherry picked from commit d5d84c3) Co-authored-by: Peter Bierma <[email protected]>
1 parent eb692d9 commit bcdd10d

File tree

7 files changed

+73
-38
lines changed

7 files changed

+73
-38
lines changed

Doc/c-api/init.rst

+9
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,15 @@ Initializing and finalizing the interpreter
557557
customized Python that always runs in isolated mode using
558558
:c:func:`Py_RunMain`.
559559
560+
.. c:function:: int PyUnstable_AtExit(PyInterpreterState *interp, void (*func)(void *), void *data)
561+
562+
Register an :mod:`atexit` callback for the target interpreter *interp*.
563+
This is similar to :c:func:`Py_AtExit`, but takes an explicit interpreter and
564+
data pointer for the callback.
565+
566+
The :term:`GIL` must be held for *interp*.
567+
568+
.. versionadded:: 3.13
560569
561570
Process-wide parameters
562571
=======================

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,6 +44,7 @@ typedef struct {
4444

4545
struct atexit_state {
4646
atexit_callback *ll_callbacks;
47+
// Kept for ABI compatibility--do not use! (See GH-127791.)
4748
atexit_callback *last_ll_callback;
4849

4950
// XXX The rest of the state could be moved to the atexit module state
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

+49
Original file line numberDiff line numberDiff line change
@@ -3338,6 +3338,54 @@ pyeval_getlocals(PyObject *module, PyObject *Py_UNUSED(args))
33383338
return Py_XNewRef(PyEval_GetLocals());
33393339
}
33403340

3341+
struct atexit_data {
3342+
int called;
3343+
PyThreadState *tstate;
3344+
PyInterpreterState *interp;
3345+
};
3346+
3347+
static void
3348+
atexit_callback(void *data)
3349+
{
3350+
struct atexit_data *at_data = (struct atexit_data *)data;
3351+
// Ensure that the callback is from the same interpreter
3352+
assert(PyThreadState_Get() == at_data->tstate);
3353+
assert(PyInterpreterState_Get() == at_data->interp);
3354+
++at_data->called;
3355+
}
3356+
3357+
static PyObject *
3358+
test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
3359+
{
3360+
PyThreadState *oldts = PyThreadState_Swap(NULL);
3361+
PyThreadState *tstate = Py_NewInterpreter();
3362+
3363+
struct atexit_data data = {0};
3364+
data.tstate = PyThreadState_Get();
3365+
data.interp = PyInterpreterState_Get();
3366+
3367+
int amount = 10;
3368+
for (int i = 0; i < amount; ++i)
3369+
{
3370+
int res = PyUnstable_AtExit(tstate->interp, atexit_callback, (void *)&data);
3371+
if (res < 0) {
3372+
Py_EndInterpreter(tstate);
3373+
PyThreadState_Swap(oldts);
3374+
PyErr_SetString(PyExc_RuntimeError, "atexit callback failed");
3375+
return NULL;
3376+
}
3377+
}
3378+
3379+
Py_EndInterpreter(tstate);
3380+
PyThreadState_Swap(oldts);
3381+
3382+
if (data.called != amount) {
3383+
PyErr_SetString(PyExc_RuntimeError, "atexit callback not called");
3384+
return NULL;
3385+
}
3386+
Py_RETURN_NONE;
3387+
}
3388+
33413389
static PyMethodDef TestMethods[] = {
33423390
{"set_errno", set_errno, METH_VARARGS},
33433391
{"test_config", test_config, METH_NOARGS},
@@ -3483,6 +3531,7 @@ static PyMethodDef TestMethods[] = {
34833531
{"function_set_warning", function_set_warning, METH_NOARGS},
34843532
{"test_critical_sections", test_critical_sections, METH_NOARGS},
34853533
{"pyeval_getlocals", pyeval_getlocals, METH_NOARGS},
3534+
{"test_atexit", test_atexit, METH_NOARGS},
34863535
{NULL, NULL} /* sentinel */
34873536
};
34883537

Modules/_testinternalcapi.c

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

1237-
1238-
struct atexit_data {
1239-
int called;
1240-
};
1241-
1242-
static void
1243-
callback(void *data)
1244-
{
1245-
((struct atexit_data *)data)->called += 1;
1246-
}
1247-
1248-
static PyObject *
1249-
test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
1250-
{
1251-
PyThreadState *oldts = PyThreadState_Swap(NULL);
1252-
PyThreadState *tstate = Py_NewInterpreter();
1253-
1254-
struct atexit_data data = {0};
1255-
int res = PyUnstable_AtExit(tstate->interp, callback, (void *)&data);
1256-
Py_EndInterpreter(tstate);
1257-
PyThreadState_Swap(oldts);
1258-
if (res < 0) {
1259-
return NULL;
1260-
}
1261-
1262-
if (data.called == 0) {
1263-
PyErr_SetString(PyExc_RuntimeError, "atexit callback not called");
1264-
return NULL;
1265-
}
1266-
Py_RETURN_NONE;
1267-
}
1268-
1269-
12701237
static PyObject *
12711238
test_pyobject_is_freed(const char *test_name, PyObject *op)
12721239
{
@@ -2065,7 +2032,6 @@ static PyMethodDef module_functions[] = {
20652032
{"_PyTraceMalloc_GetTraceback", tracemalloc_get_traceback, METH_VARARGS},
20662033
{"test_tstate_capi", test_tstate_capi, METH_NOARGS, NULL},
20672034
{"_PyUnicode_TransformDecimalAndSpaceToASCII", unicode_transformdecimalandspacetoascii, METH_O},
2068-
{"test_atexit", test_atexit, METH_NOARGS},
20692035
{"check_pyobject_forbidden_bytes_is_freed",
20702036
check_pyobject_forbidden_bytes_is_freed, METH_NOARGS},
20712037
{"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)