Skip to content

Commit f3cead4

Browse files
gh-116946: Implement the GC protocol for _tkinter tkapp and tktimertoken
The _tkinter.tkapp and _tkinter.tktimertoken types never implemented the garbage collector protocol, so reference cycles through an interpreter's trace function or a timer handler's callback could not be collected. A pending timer is kept alive by the Tcl event loop, which fires it even after the Python token is dropped, so it is treated as a GC root (only its callback is traversed) and collecting it never cancels a live timer. The GC slots use a plain cast rather than the type-checking macro, since the collector may visit a surviving object at shutdown after module clearing has reset the global type pointers. Deallocation cancels any pending timer so its callback cannot run on freed memory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 05679f3 commit f3cead4

3 files changed

Lines changed: 102 additions & 13 deletions

File tree

Lib/test/test_tkinter/test_misc.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import collections.abc
22
import functools
3+
import gc
34
import platform
45
import sys
6+
import time
57
import unittest
68
import tkinter
79
from tkinter import TclError
810
import enum
911
from test import support
1012
from test.support import os_helper
13+
from test.support.script_helper import assert_python_ok
1114
from test.test_tkinter.support import setUpModule # noqa: F401
1215
from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
1316
requires_tk, get_tk_patchlevel,
@@ -350,6 +353,36 @@ def callback():
350353
self.root.deletecommand(name)
351354
self.assertRaises(TclError, self.root.tk.call, name)
352355

356+
def test_gc_protocol(self):
357+
# gh-116946: _tkinter objects implement the GC protocol.
358+
self.assertTrue(gc.is_tracked(self.root))
359+
tok = self.root.tk.createtimerhandler(10_000_000, lambda: None)
360+
try:
361+
self.assertTrue(gc.is_tracked(tok))
362+
finally:
363+
tok.deletetimerhandler()
364+
365+
def test_timer_fires_after_gc(self):
366+
# gh-116946: a pending timer is kept alive by the Tcl event loop, not by
367+
# the garbage collector, so collecting it must not cancel it -- it must
368+
# still fire even when the Python token has been dropped.
369+
fired = []
370+
self.root.tk.createtimerhandler(1, lambda: fired.append(1))
371+
support.gc_collect()
372+
deadline = time.monotonic() + support.SHORT_TIMEOUT
373+
while not fired and time.monotonic() < deadline:
374+
self.root.update()
375+
self.assertEqual(fired, [1])
376+
377+
def test_pending_timer_at_shutdown(self):
378+
# gh-116946: the final garbage collection at interpreter shutdown must
379+
# not crash when it visits a timer that is still pending (its type has
380+
# already been cleared by the module's tp_clear).
381+
assert_python_ok('-c',
382+
'import tkinter\n'
383+
'interp = tkinter.Tcl()\n'
384+
'interp.tk.createtimerhandler(10_000_000, lambda: None)\n')
385+
353386
def test_option(self):
354387
self.addCleanup(self.root.option_clear)
355388
self.root.option_add('*Button.background', 'red')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The internal :mod:`!_tkinter` ``tkapp`` and ``tktimertoken`` types now
2+
implement the garbage collector protocol, so reference cycles involving a
3+
Tcl interpreter or a timer handler can be collected.

Modules/_tkinter.c

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,8 @@ Tkapp_New(const char *screenName, const char *className,
626626
TkappObject *v;
627627
char *argv0;
628628

629-
v = PyObject_New(TkappObject, (PyTypeObject *) Tkapp_Type);
629+
PyTypeObject *tp = (PyTypeObject *)Tkapp_Type;
630+
v = (TkappObject *)tp->tp_alloc(tp, 0);
630631
if (v == NULL)
631632
return NULL;
632633

@@ -2810,7 +2811,8 @@ Tktt_New(PyObject *func)
28102811
{
28112812
TkttObject *v;
28122813

2813-
v = PyObject_New(TkttObject, (PyTypeObject *) Tktt_Type);
2814+
PyTypeObject *tp = (PyTypeObject *)Tktt_Type;
2815+
v = (TkttObject *)tp->tp_alloc(tp, 0);
28142816
if (v == NULL)
28152817
return NULL;
28162818

@@ -2821,16 +2823,41 @@ Tktt_New(PyObject *func)
28212823
return (TkttObject*)Py_NewRef(v);
28222824
}
28232825

2824-
static void
2825-
Tktt_Dealloc(PyObject *self)
2826+
/* Plain cast, not TkttObject_CAST: the GC can run at shutdown after
2827+
module_clear() has cleared the global Tktt_Type the macro checks against. */
2828+
2829+
static int
2830+
Tktt_Clear(PyObject *op)
28262831
{
2827-
TkttObject *v = TkttObject_CAST(self);
2828-
PyObject *func = v->func;
2829-
PyObject *tp = (PyObject *) Py_TYPE(self);
2832+
TkttObject *self = (TkttObject *)op;
2833+
Py_CLEAR(self->func);
2834+
return 0;
2835+
}
28302836

2831-
Py_XDECREF(func);
2837+
static int
2838+
Tktt_Traverse(PyObject *op, visitproc visit, void *arg)
2839+
{
2840+
TkttObject *self = (TkttObject *)op;
2841+
Py_VISIT(Py_TYPE(op));
2842+
/* Not the extra reference of a pending timer (see Tktt_New): it is owned
2843+
by the Tcl event loop, so the timer is a GC root, not part of a cycle. */
2844+
Py_VISIT(self->func);
2845+
return 0;
2846+
}
28322847

2833-
PyObject_Free(self);
2848+
static void
2849+
Tktt_Dealloc(PyObject *op)
2850+
{
2851+
TkttObject *self = (TkttObject *)op; /* see GC slots above */
2852+
PyTypeObject *tp = Py_TYPE(op);
2853+
PyObject_GC_UnTrack(op);
2854+
/* Cancel any pending timer so its callback cannot fire on freed memory. */
2855+
if (self->token != NULL) {
2856+
Tcl_DeleteTimerHandler(self->token);
2857+
self->token = NULL;
2858+
}
2859+
Py_XDECREF(self->func);
2860+
tp->tp_free(op);
28342861
Py_DECREF(tp);
28352862
}
28362863

@@ -3124,17 +3151,37 @@ _tkinter_tkapp_willdispatch_impl(TkappObject *self)
31243151

31253152
/**** Tkapp Type Methods ****/
31263153

3154+
/* Plain casts -- see the Tktt GC slots above. */
3155+
3156+
static int
3157+
Tkapp_Clear(PyObject *op)
3158+
{
3159+
TkappObject *self = (TkappObject *)op;
3160+
Py_CLEAR(self->trace);
3161+
return 0;
3162+
}
3163+
3164+
static int
3165+
Tkapp_Traverse(PyObject *op, visitproc visit, void *arg)
3166+
{
3167+
TkappObject *self = (TkappObject *)op;
3168+
Py_VISIT(Py_TYPE(op));
3169+
Py_VISIT(self->trace);
3170+
return 0;
3171+
}
3172+
31273173
static void
31283174
Tkapp_Dealloc(PyObject *op)
31293175
{
3130-
TkappObject *self = TkappObject_CAST(op);
3131-
PyTypeObject *tp = Py_TYPE(self);
3176+
TkappObject *self = (TkappObject *)op;
3177+
PyTypeObject *tp = Py_TYPE(op);
3178+
PyObject_GC_UnTrack(op);
31323179
/*CHECK_TCL_APPARTMENT;*/
31333180
ENTER_TCL
31343181
Tcl_DeleteInterp(Tkapp_Interp(self));
31353182
LEAVE_TCL
3136-
Py_XDECREF(self->trace);
3137-
PyObject_Free(self);
3183+
(void)Tkapp_Clear(op);
3184+
tp->tp_free(op);
31383185
Py_DECREF(tp);
31393186
DisableEventHook();
31403187
}
@@ -3341,6 +3388,8 @@ static PyMethodDef Tktt_methods[] =
33413388

33423389
static PyType_Slot Tktt_Type_slots[] = {
33433390
{Py_tp_dealloc, Tktt_Dealloc},
3391+
{Py_tp_traverse, Tktt_Traverse},
3392+
{Py_tp_clear, Tktt_Clear},
33443393
{Py_tp_repr, Tktt_Repr},
33453394
{Py_tp_methods, Tktt_methods},
33463395
{0, 0}
@@ -3353,6 +3402,7 @@ static PyType_Spec Tktt_Type_spec = {
33533402
Py_TPFLAGS_DEFAULT
33543403
| Py_TPFLAGS_DISALLOW_INSTANTIATION
33553404
| Py_TPFLAGS_IMMUTABLETYPE
3405+
| Py_TPFLAGS_HAVE_GC
33563406
),
33573407
.slots = Tktt_Type_slots,
33583408
};
@@ -3400,6 +3450,8 @@ static PyMethodDef Tkapp_methods[] =
34003450

34013451
static PyType_Slot Tkapp_Type_slots[] = {
34023452
{Py_tp_dealloc, Tkapp_Dealloc},
3453+
{Py_tp_traverse, Tkapp_Traverse},
3454+
{Py_tp_clear, Tkapp_Clear},
34033455
{Py_tp_methods, Tkapp_methods},
34043456
{0, 0}
34053457
};
@@ -3412,6 +3464,7 @@ static PyType_Spec Tkapp_Type_spec = {
34123464
Py_TPFLAGS_DEFAULT
34133465
| Py_TPFLAGS_DISALLOW_INSTANTIATION
34143466
| Py_TPFLAGS_IMMUTABLETYPE
3467+
| Py_TPFLAGS_HAVE_GC
34153468
),
34163469
.slots = Tkapp_Type_slots,
34173470
};

0 commit comments

Comments
 (0)