Skip to content

Commit f6d16a0

Browse files
authored
gh-149216: Notify type watchers on heap type deallocation (GH-149236)
Authored-by: Anuj Bharambe <anujnitinb@gmail.com>
1 parent 1147810 commit f6d16a0

5 files changed

Lines changed: 88 additions & 1 deletion

File tree

Doc/c-api/type.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,16 @@ Type Objects
110110
:c:func:`!_PyType_Lookup` is not called on *type* between the modifications;
111111
this is an implementation detail and subject to change.)
112112
113+
The callback is also invoked when a watched heap type is deallocated.
114+
113115
An extension should never call ``PyType_Watch`` with a *watcher_id* that was
114116
not returned to it by a previous call to :c:func:`PyType_AddWatcher`.
115117
116118
.. versionadded:: 3.12
117119
120+
.. versionchanged:: 3.15
121+
The callback is now also invoked when a watched heap type is deallocated.
122+
118123
119124
.. c:function:: int PyType_Unwatch(int watcher_id, PyObject *type)
120125
@@ -138,8 +143,17 @@ Type Objects
138143
called on *type* or any type in its MRO; violating this rule could cause
139144
infinite recursion.
140145
146+
The callback may be called during type deallocation. In this case, the type
147+
object is temporarily resurrected (its reference count is at least 1) and all
148+
its attributes are still valid. However, the callback should not store new
149+
strong references to the type, as this would resurrect the object and prevent
150+
its deallocation.
151+
141152
.. versionadded:: 3.12
142153
154+
.. versionchanged:: 3.15
155+
The callback may now be called during deallocation of a watched heap type.
156+
143157
144158
.. c:function:: int PyType_HasFeature(PyTypeObject *o, int feature)
145159

Lib/test/test_capi/test_watchers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class TestTypeWatchers(unittest.TestCase):
208208
TYPES = 0 # appends modified types to global event list
209209
ERROR = 1 # unconditionally sets and signals a RuntimeException
210210
WRAP = 2 # appends modified type wrapped in list to global event list
211+
NAME = 3 # appends type name (string) to global event list
211212

212213
# duplicating the C constant
213214
TYPE_MAX_WATCHERS = 8
@@ -377,6 +378,27 @@ def test_clear_unassigned_watcher_id(self):
377378
with self.assertRaisesRegex(ValueError, r"No type watcher set for ID 1"):
378379
self.clear_watcher(1)
379380

381+
def test_watch_type_dealloc(self):
382+
# Use the NAME watcher (kind=3) which records the type's name as a
383+
# string, avoiding any reference to the type object itself during
384+
# deallocation.
385+
with self.watcher(kind=self.NAME) as wid:
386+
class MyTestType: pass
387+
self.watch(wid, MyTestType)
388+
del MyTestType
389+
gc_collect()
390+
events = _testcapi.get_type_modified_events()
391+
self.assertIn("MyTestType", events)
392+
393+
def test_watch_type_dealloc_error(self):
394+
with self.watcher(kind=self.ERROR) as wid:
395+
class MyTestType2: pass
396+
self.watch(wid, MyTestType2)
397+
with catch_unraisable_exception() as cm:
398+
del MyTestType2
399+
gc_collect()
400+
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
401+
380402
def test_no_more_ids_available(self):
381403
with self.assertRaisesRegex(RuntimeError, r"no more type watcher IDs"):
382404
with ExitStack() as stack:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:c:type:`PyType_WatchCallback` callbacks registered via
2+
:c:func:`PyType_AddWatcher` are now also invoked when a watched heap type is
3+
deallocated. Previously, type watchers were only notified of modifications,
4+
which could cause stale references when a type was freed and its address was
5+
reused.

Modules/_testcapi/watchers.c

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,32 @@ type_modified_callback_error(PyTypeObject *type)
212212
return -1;
213213
}
214214

215+
static int
216+
type_modified_callback_name(PyTypeObject *type)
217+
{
218+
assert(PyList_Check(g_type_modified_events));
219+
PyObject *name = PyUnicode_FromString(type->tp_name);
220+
if (name == NULL) {
221+
return -1;
222+
}
223+
if (PyList_Append(g_type_modified_events, name) < 0) {
224+
Py_DECREF(name);
225+
return -1;
226+
}
227+
Py_DECREF(name);
228+
return 0;
229+
}
230+
215231
static PyObject *
216232
add_type_watcher(PyObject *self, PyObject *kind)
217233
{
218234
int watcher_id;
219235
assert(PyLong_Check(kind));
220236
long kind_l = PyLong_AsLong(kind);
221-
if (kind_l == 2) {
237+
if (kind_l == 3) {
238+
watcher_id = PyType_AddWatcher(type_modified_callback_name);
239+
}
240+
else if (kind_l == 2) {
222241
watcher_id = PyType_AddWatcher(type_modified_callback_wrap);
223242
}
224243
else if (kind_l == 1) {

Objects/typeobject.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6940,6 +6940,33 @@ type_dealloc(PyObject *self)
69406940
// Assert this is a heap-allocated type object
69416941
_PyObject_ASSERT((PyObject *)type, type->tp_flags & Py_TPFLAGS_HEAPTYPE);
69426942

6943+
// Notify type watchers before teardown. The type object is still fully
6944+
// intact at this point (dict, bases, mro, name are all valid), so
6945+
// callbacks can safely inspect it.
6946+
if (type->tp_watched) {
6947+
_PyObject_ResurrectStart(self);
6948+
PyInterpreterState *interp = _PyInterpreterState_GET();
6949+
int bits = type->tp_watched;
6950+
int i = 0;
6951+
while (bits) {
6952+
assert(i < TYPE_MAX_WATCHERS);
6953+
if (bits & 1) {
6954+
PyType_WatchCallback cb = interp->type_watchers[i];
6955+
if (cb && (cb(type) < 0)) {
6956+
PyErr_FormatUnraisable(
6957+
"Exception ignored in type watcher callback #%d "
6958+
"for %R",
6959+
i, type);
6960+
}
6961+
}
6962+
i++;
6963+
bits >>= 1;
6964+
}
6965+
if (_PyObject_ResurrectEnd(self)) {
6966+
return; // callback resurrected the object
6967+
}
6968+
}
6969+
69436970
_PyObject_GC_UNTRACK(type);
69446971
type_dealloc_common(type);
69456972

0 commit comments

Comments
 (0)