Skip to content

Commit afec2c7

Browse files
authored
[3.13] gh-116946: fully implement GC protocol for _curses_panel.panel (GH-138333) (#138428)
[3.14] gh-116946: fully implement GC protocol for `_curses_panel.panel` (GH-138333) This commit fixes possible reference loops via `panel.set_userptr` by implementing `tp_clear` and `tp_traverse` for panel objects. (cherry picked from commit 572df47)
1 parent e7bb98a commit afec2c7

File tree

1 file changed

+76
-13
lines changed

1 file changed

+76
-13
lines changed

Modules/_curses_panel.c

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ typedef struct {
3232
PyTypeObject *PyCursesPanel_Type;
3333
} _curses_panel_state;
3434

35+
typedef struct PyCursesPanelObject PyCursesPanelObject;
36+
3537
static inline _curses_panel_state *
3638
get_curses_panel_state(PyObject *module)
3739
{
@@ -40,6 +42,25 @@ get_curses_panel_state(PyObject *module)
4042
return (_curses_panel_state *)state;
4143
}
4244

45+
static inline _curses_panel_state *
46+
get_curses_panel_state_by_panel(PyCursesPanelObject *panel)
47+
{
48+
/*
49+
* Note: 'state' may be NULL if Py_TYPE(panel) is not a heap
50+
* type associated with this module, but the compiler would
51+
* have likely already complained with an "invalid pointer
52+
* type" at compile-time.
53+
*
54+
* To make it more robust, all functions recovering a module's
55+
* state from an object should expect to return NULL with an
56+
* exception set (in contrast to functions recovering a module's
57+
* state from a module itself).
58+
*/
59+
void *state = PyType_GetModuleState(Py_TYPE(panel));
60+
assert(state != NULL);
61+
return (_curses_panel_state *)state;
62+
}
63+
4364
static int
4465
_curses_panel_clear(PyObject *mod)
4566
{
@@ -95,12 +116,14 @@ PyCursesCheckERR(_curses_panel_state *state, int code, const char *fname)
95116

96117
/* Definition of the panel object and panel type */
97118

98-
typedef struct {
119+
typedef struct PyCursesPanelObject {
99120
PyObject_HEAD
100121
PANEL *pan;
101122
PyCursesWindowObject *wo; /* for reference counts */
102123
} PyCursesPanelObject;
103124

125+
#define _PyCursesPanelObject_CAST(op) ((PyCursesPanelObject *)(op))
126+
104127
/* Some helper functions. The problem is that there's always a window
105128
associated with a panel. To ensure that Python's GC doesn't pull
106129
this window from under our feet we need to keep track of references
@@ -260,8 +283,11 @@ static PyObject *
260283
PyCursesPanel_New(_curses_panel_state *state, PANEL *pan,
261284
PyCursesWindowObject *wo)
262285
{
263-
PyCursesPanelObject *po = PyObject_New(PyCursesPanelObject,
264-
state->PyCursesPanel_Type);
286+
assert(state != NULL);
287+
PyTypeObject *type = state->PyCursesPanel_Type;
288+
assert(type != NULL);
289+
assert(type->tp_alloc != NULL);
290+
PyCursesPanelObject *po = (PyCursesPanelObject *)type->tp_alloc(type, 0);
265291
if (po == NULL) {
266292
return NULL;
267293
}
@@ -276,26 +302,57 @@ PyCursesPanel_New(_curses_panel_state *state, PANEL *pan,
276302
return (PyObject *)po;
277303
}
278304

305+
static int
306+
PyCursesPanel_Clear(PyObject *op)
307+
{
308+
PyCursesPanelObject *self = _PyCursesPanelObject_CAST(op);
309+
PyObject *extra = (PyObject *)panel_userptr(self->pan);
310+
if (extra != NULL) {
311+
Py_DECREF(extra);
312+
if (set_panel_userptr(self->pan, NULL) == ERR) {
313+
_curses_panel_state *state = get_curses_panel_state_by_panel(self);
314+
PyErr_SetString(state->PyCursesError,
315+
"set_panel_userptr() returned ERR");
316+
return -1;
317+
}
318+
}
319+
// self->wo should not be cleared because an associated WINDOW may exist
320+
return 0;
321+
}
322+
279323
static void
280-
PyCursesPanel_Dealloc(PyCursesPanelObject *po)
324+
PyCursesPanel_Dealloc(PyObject *self)
281325
{
282-
PyObject *tp, *obj;
326+
PyTypeObject *tp = Py_TYPE(self);
327+
PyObject_GC_UnTrack(self);
283328

284-
tp = (PyObject *) Py_TYPE(po);
285-
obj = (PyObject *) panel_userptr(po->pan);
286-
if (obj) {
287-
(void)set_panel_userptr(po->pan, NULL);
288-
Py_DECREF(obj);
329+
PyCursesPanelObject *po = _PyCursesPanelObject_CAST(self);
330+
if (PyCursesPanel_Clear(self) < 0) {
331+
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
332+
}
333+
if (del_panel(po->pan) == ERR && !PyErr_Occurred()) {
334+
_curses_panel_state *state = get_curses_panel_state_by_panel(po);
335+
PyErr_SetString(state->PyCursesError, "del_panel() returned ERR");
336+
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
289337
}
290-
(void)del_panel(po->pan);
291338
if (po->wo != NULL) {
292339
Py_DECREF(po->wo);
293340
remove_lop(po);
294341
}
295-
PyObject_Free(po);
342+
tp->tp_free(po);
296343
Py_DECREF(tp);
297344
}
298345

346+
static int
347+
PyCursesPanel_Traverse(PyObject *op, visitproc visit, void *arg)
348+
{
349+
PyCursesPanelObject *self = _PyCursesPanelObject_CAST(op);
350+
Py_VISIT(Py_TYPE(op));
351+
Py_VISIT(panel_userptr(self->pan));
352+
Py_VISIT(self->wo);
353+
return 0;
354+
}
355+
299356
/* panel_above(NULL) returns the bottom panel in the stack. To get
300357
this behaviour we use curses.panel.bottom_panel(). */
301358
/*[clinic input]
@@ -517,15 +574,21 @@ static PyMethodDef PyCursesPanel_Methods[] = {
517574
/* -------------------------------------------------------*/
518575

519576
static PyType_Slot PyCursesPanel_Type_slots[] = {
577+
{Py_tp_clear, PyCursesPanel_Clear},
520578
{Py_tp_dealloc, PyCursesPanel_Dealloc},
579+
{Py_tp_traverse, PyCursesPanel_Traverse},
521580
{Py_tp_methods, PyCursesPanel_Methods},
522581
{0, 0},
523582
};
524583

525584
static PyType_Spec PyCursesPanel_Type_spec = {
526585
.name = "_curses_panel.panel",
527586
.basicsize = sizeof(PyCursesPanelObject),
528-
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION,
587+
.flags = (
588+
Py_TPFLAGS_DEFAULT
589+
| Py_TPFLAGS_DISALLOW_INSTANTIATION
590+
| Py_TPFLAGS_HAVE_GC
591+
),
529592
.slots = PyCursesPanel_Type_slots
530593
};
531594

0 commit comments

Comments
 (0)