:c:func:`!_PyType_Lookup` is not called on *type* between the modifications;
this is an implementation detail and subject to change.)
+ The callback is also invoked when a watched heap type is deallocated.
+
An extension should never call ``PyType_Watch`` with a *watcher_id* that was
not returned to it by a previous call to :c:func:`PyType_AddWatcher`.
.. versionadded:: 3.12
+ .. versionchanged:: 3.15
+ The callback is now also invoked when a watched heap type is deallocated.
+
.. c:function:: int PyType_Unwatch(int watcher_id, PyObject *type)
called on *type* or any type in its MRO; violating this rule could cause
infinite recursion.
+ The callback may be called during type deallocation. In this case, the type
+ object is temporarily resurrected (its reference count is at least 1) and all
+ its attributes are still valid. However, the callback should not store new
+ strong references to the type, as this would resurrect the object and prevent
+ its deallocation.
+
.. versionadded:: 3.12
+ .. versionchanged:: 3.15
+ The callback may now be called during deallocation of a watched heap type.
+
.. c:function:: int PyType_HasFeature(PyTypeObject *o, int feature)
TYPES = 0 # appends modified types to global event list
ERROR = 1 # unconditionally sets and signals a RuntimeException
WRAP = 2 # appends modified type wrapped in list to global event list
+ NAME = 3 # appends type name (string) to global event list
# duplicating the C constant
TYPE_MAX_WATCHERS = 8
with self.assertRaisesRegex(ValueError, r"No type watcher set for ID 1"):
self.clear_watcher(1)
+ def test_watch_type_dealloc(self):
+ # Use the NAME watcher (kind=3) which records the type's name as a
+ # string, avoiding any reference to the type object itself during
+ # deallocation.
+ with self.watcher(kind=self.NAME) as wid:
+ class MyTestType: pass
+ self.watch(wid, MyTestType)
+ del MyTestType
+ gc_collect()
+ events = _testcapi.get_type_modified_events()
+ self.assertIn("MyTestType", events)
+
+ def test_watch_type_dealloc_error(self):
+ with self.watcher(kind=self.ERROR) as wid:
+ class MyTestType2: pass
+ self.watch(wid, MyTestType2)
+ with catch_unraisable_exception() as cm:
+ del MyTestType2
+ gc_collect()
+ self.assertEqual(str(cm.unraisable.exc_value), "boom!")
+
def test_no_more_ids_available(self):
with self.assertRaisesRegex(RuntimeError, r"no more type watcher IDs"):
with ExitStack() as stack:
--- /dev/null
+:c:type:`PyType_WatchCallback` callbacks registered via
+:c:func:`PyType_AddWatcher` are now also invoked when a watched heap type is
+deallocated. Previously, type watchers were only notified of modifications,
+which could cause stale references when a type was freed and its address was
+reused.
return -1;
}
+static int
+type_modified_callback_name(PyTypeObject *type)
+{
+ assert(PyList_Check(g_type_modified_events));
+ PyObject *name = PyUnicode_FromString(type->tp_name);
+ if (name == NULL) {
+ return -1;
+ }
+ if (PyList_Append(g_type_modified_events, name) < 0) {
+ Py_DECREF(name);
+ return -1;
+ }
+ Py_DECREF(name);
+ return 0;
+}
+
static PyObject *
add_type_watcher(PyObject *self, PyObject *kind)
{
int watcher_id;
assert(PyLong_Check(kind));
long kind_l = PyLong_AsLong(kind);
- if (kind_l == 2) {
+ if (kind_l == 3) {
+ watcher_id = PyType_AddWatcher(type_modified_callback_name);
+ }
+ else if (kind_l == 2) {
watcher_id = PyType_AddWatcher(type_modified_callback_wrap);
}
else if (kind_l == 1) {
// Assert this is a heap-allocated type object
_PyObject_ASSERT((PyObject *)type, type->tp_flags & Py_TPFLAGS_HEAPTYPE);
+ // Notify type watchers before teardown. The type object is still fully
+ // intact at this point (dict, bases, mro, name are all valid), so
+ // callbacks can safely inspect it.
+ if (type->tp_watched) {
+ _PyObject_ResurrectStart(self);
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+ int bits = type->tp_watched;
+ int i = 0;
+ while (bits) {
+ assert(i < TYPE_MAX_WATCHERS);
+ if (bits & 1) {
+ PyType_WatchCallback cb = interp->type_watchers[i];
+ if (cb && (cb(type) < 0)) {
+ PyErr_FormatUnraisable(
+ "Exception ignored in type watcher callback #%d "
+ "for %R",
+ i, type);
+ }
+ }
+ i++;
+ bits >>= 1;
+ }
+ if (_PyObject_ResurrectEnd(self)) {
+ return; // callback resurrected the object
+ }
+ }
+
_PyObject_GC_UNTRACK(type);
type_dealloc_common(type);