mirror of
https://github.com/python/cpython.git
synced 2026-05-06 04:37:33 -04:00
gh-149216: Notify type watchers on heap type deallocation (GH-149236)
Authored-by: Anuj Bharambe <anujnitinb@gmail.com>
This commit is contained in:
committed by
GitHub
parent
114781040c
commit
f6d16a0d70
@@ -110,11 +110,16 @@ Type Objects
|
||||
: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)
|
||||
|
||||
@@ -138,8 +143,17 @@ Type Objects
|
||||
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)
|
||||
|
||||
|
||||
@@ -208,6 +208,7 @@ class TestTypeWatchers(unittest.TestCase):
|
||||
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
|
||||
@@ -377,6 +378,27 @@ class TestTypeWatchers(unittest.TestCase):
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
: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.
|
||||
@@ -212,13 +212,32 @@ type_modified_callback_error(PyTypeObject *type)
|
||||
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) {
|
||||
|
||||
@@ -6940,6 +6940,33 @@ type_dealloc(PyObject *self)
|
||||
// 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user