Skip to content

Commit 03089fd

Browse files
gh-101659: Add _Py_AtExit() (gh-103298)
The function is like Py_AtExit() but for a single interpreter. This is a companion to the atexit module's register() function, taking a C callback instead of a Python one. We also update the _xxinterpchannels module to use _Py_AtExit(), which is the motivating case. (This is inspired by pain points felt while working on gh-101660.)
1 parent 4ec8dd1 commit 03089fd

13 files changed

+269
-68
lines changed

Include/cpython/pylifecycle.h

+4
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,7 @@ PyAPI_FUNC(char *) _Py_SetLocaleFromEnv(int category);
6565
PyAPI_FUNC(PyStatus) _Py_NewInterpreterFromConfig(
6666
PyThreadState **tstate_p,
6767
const _PyInterpreterConfig *config);
68+
69+
typedef void (*atexit_datacallbackfunc)(void *);
70+
PyAPI_FUNC(int) _Py_AtExit(
71+
PyInterpreterState *, atexit_datacallbackfunc, void *);

Include/internal/pycore_atexit.h

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#ifndef Py_INTERNAL_ATEXIT_H
2+
#define Py_INTERNAL_ATEXIT_H
3+
#ifdef __cplusplus
4+
extern "C" {
5+
#endif
6+
7+
#ifndef Py_BUILD_CORE
8+
# error "this header requires Py_BUILD_CORE define"
9+
#endif
10+
11+
12+
//###############
13+
// runtime atexit
14+
15+
typedef void (*atexit_callbackfunc)(void);
16+
17+
struct _atexit_runtime_state {
18+
#define NEXITFUNCS 32
19+
atexit_callbackfunc callbacks[NEXITFUNCS];
20+
int ncallbacks;
21+
};
22+
23+
24+
//###################
25+
// interpreter atexit
26+
27+
struct atexit_callback;
28+
typedef struct atexit_callback {
29+
atexit_datacallbackfunc func;
30+
void *data;
31+
struct atexit_callback *next;
32+
} atexit_callback;
33+
34+
typedef struct {
35+
PyObject *func;
36+
PyObject *args;
37+
PyObject *kwargs;
38+
} atexit_py_callback;
39+
40+
struct atexit_state {
41+
atexit_callback *ll_callbacks;
42+
atexit_callback *last_ll_callback;
43+
44+
// XXX The rest of the state could be moved to the atexit module state
45+
// and a low-level callback added for it during module exec.
46+
// For the moment we leave it here.
47+
atexit_py_callback **callbacks;
48+
int ncallbacks;
49+
int callback_len;
50+
};
51+
52+
53+
#ifdef __cplusplus
54+
}
55+
#endif
56+
#endif /* !Py_INTERNAL_ATEXIT_H */

Include/internal/pycore_interp.h

+2-15
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ extern "C" {
1010

1111
#include <stdbool.h>
1212

13-
#include "pycore_atomic.h" // _Py_atomic_address
1413
#include "pycore_ast_state.h" // struct ast_state
14+
#include "pycore_atexit.h" // struct atexit_state
15+
#include "pycore_atomic.h" // _Py_atomic_address
1516
#include "pycore_ceval_state.h" // struct _ceval_state
1617
#include "pycore_code.h" // struct callable_cache
1718
#include "pycore_context.h" // struct _Py_context_state
@@ -32,20 +33,6 @@ extern "C" {
3233
#include "pycore_warnings.h" // struct _warnings_runtime_state
3334

3435

35-
// atexit state
36-
typedef struct {
37-
PyObject *func;
38-
PyObject *args;
39-
PyObject *kwargs;
40-
} atexit_callback;
41-
42-
struct atexit_state {
43-
atexit_callback **callbacks;
44-
int ncallbacks;
45-
int callback_len;
46-
};
47-
48-
4936
struct _Py_long_state {
5037
int max_str_digits;
5138
};

Include/internal/pycore_runtime.h

+2-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ extern "C" {
88
# error "this header requires Py_BUILD_CORE define"
99
#endif
1010

11+
#include "pycore_atexit.h" // struct atexit_runtime_state
1112
#include "pycore_atomic.h" /* _Py_atomic_address */
1213
#include "pycore_ceval_state.h" // struct _ceval_runtime_state
1314
#include "pycore_floatobject.h" // struct _Py_float_runtime_state
@@ -131,9 +132,7 @@ typedef struct pyruntimestate {
131132

132133
struct _parser_runtime_state parser;
133134

134-
#define NEXITFUNCS 32
135-
void (*exitfuncs[NEXITFUNCS])(void);
136-
int nexitfuncs;
135+
struct _atexit_runtime_state atexit;
137136

138137
struct _import_runtime_state imports;
139138
struct _ceval_runtime_state ceval;

Lib/test/test__xxinterpchannels.py

+29-11
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ def test_channel_list_interpreters_closed_send_end(self):
550550
import _xxinterpchannels as _channels
551551
_channels.close({cid}, force=True)
552552
"""))
553+
return
553554
# Both ends should raise an error.
554555
with self.assertRaises(channels.ChannelClosedError):
555556
channels.list_interpreters(cid, send=True)
@@ -673,17 +674,34 @@ def test_recv_default(self):
673674
self.assertIs(obj6, default)
674675

675676
def test_recv_sending_interp_destroyed(self):
676-
cid = channels.create()
677-
interp = interpreters.create()
678-
interpreters.run_string(interp, dedent(f"""
679-
import _xxinterpchannels as _channels
680-
_channels.send({cid}, b'spam')
681-
"""))
682-
interpreters.destroy(interp)
683-
684-
with self.assertRaisesRegex(RuntimeError,
685-
'unrecognized interpreter ID'):
686-
channels.recv(cid)
677+
with self.subTest('closed'):
678+
cid1 = channels.create()
679+
interp = interpreters.create()
680+
interpreters.run_string(interp, dedent(f"""
681+
import _xxinterpchannels as _channels
682+
_channels.send({cid1}, b'spam')
683+
"""))
684+
interpreters.destroy(interp)
685+
686+
with self.assertRaisesRegex(RuntimeError,
687+
f'channel {cid1} is closed'):
688+
channels.recv(cid1)
689+
del cid1
690+
with self.subTest('still open'):
691+
cid2 = channels.create()
692+
interp = interpreters.create()
693+
interpreters.run_string(interp, dedent(f"""
694+
import _xxinterpchannels as _channels
695+
_channels.send({cid2}, b'spam')
696+
"""))
697+
channels.send(cid2, b'eggs')
698+
interpreters.destroy(interp)
699+
700+
channels.recv(cid2)
701+
with self.assertRaisesRegex(RuntimeError,
702+
f'channel {cid2} is empty'):
703+
channels.recv(cid2)
704+
del cid2
687705

688706
def test_allowed_types(self):
689707
cid = channels.create()

Makefile.pre.in

+1
Original file line numberDiff line numberDiff line change
@@ -1660,6 +1660,7 @@ PYTHON_HEADERS= \
16601660
$(srcdir)/Include/internal/pycore_asdl.h \
16611661
$(srcdir)/Include/internal/pycore_ast.h \
16621662
$(srcdir)/Include/internal/pycore_ast_state.h \
1663+
$(srcdir)/Include/internal/pycore_atexit.h \
16631664
$(srcdir)/Include/internal/pycore_atomic.h \
16641665
$(srcdir)/Include/internal/pycore_atomic_funcs.h \
16651666
$(srcdir)/Include/internal/pycore_bitutils.h \

Modules/_testcapimodule.c

+32
Original file line numberDiff line numberDiff line change
@@ -3381,6 +3381,37 @@ test_gc_visit_objects_exit_early(PyObject *Py_UNUSED(self),
33813381
}
33823382

33833383

3384+
struct atexit_data {
3385+
int called;
3386+
};
3387+
3388+
static void
3389+
callback(void *data)
3390+
{
3391+
((struct atexit_data *)data)->called += 1;
3392+
}
3393+
3394+
static PyObject *
3395+
test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
3396+
{
3397+
PyThreadState *oldts = PyThreadState_Swap(NULL);
3398+
PyThreadState *tstate = Py_NewInterpreter();
3399+
3400+
struct atexit_data data = {0};
3401+
int res = _Py_AtExit(tstate->interp, callback, (void *)&data);
3402+
Py_EndInterpreter(tstate);
3403+
PyThreadState_Swap(oldts);
3404+
if (res < 0) {
3405+
return NULL;
3406+
}
3407+
if (data.called == 0) {
3408+
PyErr_SetString(PyExc_RuntimeError, "atexit callback not called");
3409+
return NULL;
3410+
}
3411+
Py_RETURN_NONE;
3412+
}
3413+
3414+
33843415
static PyObject *test_buildvalue_issue38913(PyObject *, PyObject *);
33853416

33863417
static PyMethodDef TestMethods[] = {
@@ -3525,6 +3556,7 @@ static PyMethodDef TestMethods[] = {
35253556
{"function_set_kw_defaults", function_set_kw_defaults, METH_VARARGS, NULL},
35263557
{"test_gc_visit_objects_basic", test_gc_visit_objects_basic, METH_NOARGS, NULL},
35273558
{"test_gc_visit_objects_exit_early", test_gc_visit_objects_exit_early, METH_NOARGS, NULL},
3559+
{"test_atexit", test_atexit, METH_NOARGS},
35283560
{NULL, NULL} /* sentinel */
35293561
};
35303562

Modules/_xxinterpchannelsmodule.c

+83-13
Original file line numberDiff line numberDiff line change
@@ -174,19 +174,7 @@ _release_xid_data(_PyCrossInterpreterData *data, int ignoreexc)
174174
}
175175
int res = _PyCrossInterpreterData_Release(data);
176176
if (res < 0) {
177-
// XXX Fix this!
178-
/* The owning interpreter is already destroyed.
179-
* Ideally, this shouldn't ever happen. When an interpreter is
180-
* about to be destroyed, we should clear out all of its objects
181-
* from every channel associated with that interpreter.
182-
* For now we hack around that to resolve refleaks, by decref'ing
183-
* the released object here, even if its the wrong interpreter.
184-
* The owning interpreter has already been destroyed
185-
* so we should be okay, especially since the currently
186-
* shareable types are all very basic, with no GC.
187-
* That said, it becomes much messier once interpreters
188-
* no longer share a GIL, so this needs to be fixed before then. */
189-
_PyCrossInterpreterData_Clear(NULL, data);
177+
/* The owning interpreter is already destroyed. */
190178
if (ignoreexc) {
191179
// XXX Emit a warning?
192180
PyErr_Clear();
@@ -489,6 +477,30 @@ _channelqueue_get(_channelqueue *queue)
489477
return _channelitem_popped(item);
490478
}
491479

480+
static void
481+
_channelqueue_drop_interpreter(_channelqueue *queue, int64_t interp)
482+
{
483+
_channelitem *prev = NULL;
484+
_channelitem *next = queue->first;
485+
while (next != NULL) {
486+
_channelitem *item = next;
487+
next = item->next;
488+
if (item->data->interp == interp) {
489+
if (prev == NULL) {
490+
queue->first = item->next;
491+
}
492+
else {
493+
prev->next = item->next;
494+
}
495+
_channelitem_free(item);
496+
queue->count -= 1;
497+
}
498+
else {
499+
prev = item;
500+
}
501+
}
502+
}
503+
492504
/* channel-interpreter associations */
493505

494506
struct _channelend;
@@ -693,6 +705,20 @@ _channelends_close_interpreter(_channelends *ends, int64_t interp, int which)
693705
return 0;
694706
}
695707

708+
static void
709+
_channelends_drop_interpreter(_channelends *ends, int64_t interp)
710+
{
711+
_channelend *end;
712+
end = _channelend_find(ends->send, interp, NULL);
713+
if (end != NULL) {
714+
_channelends_close_end(ends, end, 1);
715+
}
716+
end = _channelend_find(ends->recv, interp, NULL);
717+
if (end != NULL) {
718+
_channelends_close_end(ends, end, 0);
719+
}
720+
}
721+
696722
static void
697723
_channelends_close_all(_channelends *ends, int which, int force)
698724
{
@@ -841,6 +867,18 @@ _channel_close_interpreter(_PyChannelState *chan, int64_t interp, int end)
841867
return res;
842868
}
843869

870+
static void
871+
_channel_drop_interpreter(_PyChannelState *chan, int64_t interp)
872+
{
873+
PyThread_acquire_lock(chan->mutex, WAIT_LOCK);
874+
875+
_channelqueue_drop_interpreter(chan->queue, interp);
876+
_channelends_drop_interpreter(chan->ends, interp);
877+
chan->open = _channelends_is_open(chan->ends);
878+
879+
PyThread_release_lock(chan->mutex);
880+
}
881+
844882
static int
845883
_channel_close_all(_PyChannelState *chan, int end, int force)
846884
{
@@ -1213,6 +1251,21 @@ _channels_list_all(_channels *channels, int64_t *count)
12131251
return cids;
12141252
}
12151253

1254+
static void
1255+
_channels_drop_interpreter(_channels *channels, int64_t interp)
1256+
{
1257+
PyThread_acquire_lock(channels->mutex, WAIT_LOCK);
1258+
1259+
_channelref *ref = channels->head;
1260+
for (; ref != NULL; ref = ref->next) {
1261+
if (ref->chan != NULL) {
1262+
_channel_drop_interpreter(ref->chan, interp);
1263+
}
1264+
}
1265+
1266+
PyThread_release_lock(channels->mutex);
1267+
}
1268+
12161269
/* support for closing non-empty channels */
12171270

12181271
struct _channel_closing {
@@ -1932,6 +1985,19 @@ _global_channels(void) {
19321985
}
19331986

19341987

1988+
static void
1989+
clear_interpreter(void *data)
1990+
{
1991+
if (_globals.module_count == 0) {
1992+
return;
1993+
}
1994+
PyInterpreterState *interp = (PyInterpreterState *)data;
1995+
assert(interp == _get_current_interp());
1996+
int64_t id = PyInterpreterState_GetID(interp);
1997+
_channels_drop_interpreter(&_globals.channels, id);
1998+
}
1999+
2000+
19352001
static PyObject *
19362002
channel_create(PyObject *self, PyObject *Py_UNUSED(ignored))
19372003
{
@@ -2339,6 +2405,10 @@ module_exec(PyObject *mod)
23392405
goto error;
23402406
}
23412407

2408+
// Make sure chnnels drop objects owned by this interpreter
2409+
PyInterpreterState *interp = _get_current_interp();
2410+
_Py_AtExit(interp, clear_interpreter, (void *)interp);
2411+
23422412
return 0;
23432413

23442414
error:

Modules/_xxsubinterpretersmodule.c

+1-10
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,7 @@ _release_xid_data(_PyCrossInterpreterData *data, int ignoreexc)
6767
}
6868
int res = _PyCrossInterpreterData_Release(data);
6969
if (res < 0) {
70-
// XXX Fix this!
71-
/* The owning interpreter is already destroyed.
72-
* Ideally, this shouldn't ever happen. (It's highly unlikely.)
73-
* For now we hack around that to resolve refleaks, by decref'ing
74-
* the released object here, even if its the wrong interpreter.
75-
* The owning interpreter has already been destroyed
76-
* so we should be okay, especially since the currently
77-
* shareable types are all very basic, with no GC.
78-
* That said, it becomes much messier once interpreters
79-
* no longer share a GIL, so this needs to be fixed before then. */
70+
/* The owning interpreter is already destroyed. */
8071
_PyCrossInterpreterData_Clear(NULL, data);
8172
if (ignoreexc) {
8273
// XXX Emit a warning?

0 commit comments

Comments
 (0)