Skip to content

Commit 3330bf2

Browse files
committed
fix garbage collection in inheritance cases (#4563)
* fix garbage collection in inheritance cases * clippy fixes * more clippy fixups * newsfragment * use `get_slot` helper for reading slots * fixup abi3 case * fix new `to_object` deprecation warnings * fix MSRV build
1 parent 8b23397 commit 3330bf2

File tree

6 files changed

+395
-19
lines changed

6 files changed

+395
-19
lines changed

newsfragments/4563.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix `__traverse__` functions for base classes not being called by subclasses created with `#[pyclass(extends = ...)]`.

pyo3-macros-backend/src/pymethod.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ impl PyMethodKind {
9797
"__ior__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IOR__)),
9898
"__getbuffer__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__GETBUFFER__)),
9999
"__releasebuffer__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__RELEASEBUFFER__)),
100-
"__clear__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__CLEAR__)),
101100
// Protocols implemented through traits
102101
"__getattribute__" => {
103102
PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__GETATTRIBUTE__))
@@ -146,6 +145,7 @@ impl PyMethodKind {
146145
// Some tricky protocols which don't fit the pattern of the rest
147146
"__call__" => PyMethodKind::Proto(PyMethodProtoKind::Call),
148147
"__traverse__" => PyMethodKind::Proto(PyMethodProtoKind::Traverse),
148+
"__clear__" => PyMethodKind::Proto(PyMethodProtoKind::Clear),
149149
// Not a proto
150150
_ => PyMethodKind::Fn,
151151
}
@@ -156,6 +156,7 @@ enum PyMethodProtoKind {
156156
Slot(&'static SlotDef),
157157
Call,
158158
Traverse,
159+
Clear,
159160
SlotFragment(&'static SlotFragmentDef),
160161
}
161162

@@ -218,6 +219,9 @@ pub fn gen_py_method(
218219
PyMethodProtoKind::Traverse => {
219220
GeneratedPyMethod::Proto(impl_traverse_slot(cls, spec, ctx)?)
220221
}
222+
PyMethodProtoKind::Clear => {
223+
GeneratedPyMethod::Proto(impl_clear_slot(cls, spec, ctx)?)
224+
}
221225
PyMethodProtoKind::SlotFragment(slot_fragment_def) => {
222226
let proto = slot_fragment_def.generate_pyproto_fragment(cls, spec, ctx)?;
223227
GeneratedPyMethod::SlotTraitImpl(method.method_name, proto)
@@ -467,7 +471,7 @@ fn impl_traverse_slot(
467471
visit: #pyo3_path::ffi::visitproc,
468472
arg: *mut ::std::os::raw::c_void,
469473
) -> ::std::os::raw::c_int {
470-
#pyo3_path::impl_::pymethods::_call_traverse::<#cls>(slf, #cls::#rust_fn_ident, visit, arg)
474+
#pyo3_path::impl_::pymethods::_call_traverse::<#cls>(slf, #cls::#rust_fn_ident, visit, arg, #cls::__pymethod_traverse__)
471475
}
472476
};
473477
let slot_def = quote! {
@@ -482,6 +486,51 @@ fn impl_traverse_slot(
482486
})
483487
}
484488

489+
fn impl_clear_slot(cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx) -> syn::Result<MethodAndSlotDef> {
490+
let Ctx { pyo3_path, .. } = ctx;
491+
let (py_arg, args) = split_off_python_arg(&spec.signature.arguments);
492+
let self_type = match &spec.tp {
493+
FnType::Fn(self_type) => self_type,
494+
_ => bail_spanned!(spec.name.span() => "expected instance method for `__clear__` function"),
495+
};
496+
let mut holders = Holders::new();
497+
let slf = self_type.receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx);
498+
499+
if let [arg, ..] = args {
500+
bail_spanned!(arg.ty().span() => "`__clear__` function expected to have no arguments");
501+
}
502+
503+
let name = &spec.name;
504+
let holders = holders.init_holders(ctx);
505+
let fncall = if py_arg.is_some() {
506+
quote!(#cls::#name(#slf, py))
507+
} else {
508+
quote!(#cls::#name(#slf))
509+
};
510+
511+
let associated_method = quote! {
512+
pub unsafe extern "C" fn __pymethod_clear__(
513+
_slf: *mut #pyo3_path::ffi::PyObject,
514+
) -> ::std::os::raw::c_int {
515+
#pyo3_path::impl_::pymethods::_call_clear(_slf, |py, _slf| {
516+
#holders
517+
let result = #fncall;
518+
#pyo3_path::callback::convert(py, result)
519+
}, #cls::__pymethod_clear__)
520+
}
521+
};
522+
let slot_def = quote! {
523+
#pyo3_path::ffi::PyType_Slot {
524+
slot: #pyo3_path::ffi::Py_tp_clear,
525+
pfunc: #cls::__pymethod_clear__ as #pyo3_path::ffi::inquiry as _
526+
}
527+
};
528+
Ok(MethodAndSlotDef {
529+
associated_method,
530+
slot_def,
531+
})
532+
}
533+
485534
fn impl_py_class_attribute(
486535
cls: &syn::Type,
487536
spec: &FnSpec<'_>,

src/impl_/pymethods.rs

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ use crate::exceptions::PyStopAsyncIteration;
33
use crate::gil::LockGIL;
44
use crate::impl_::panic::PanicTrap;
55
use crate::impl_::pycell::{PyClassObject, PyClassObjectLayout};
6+
use crate::internal::get_slot::{get_slot, TP_BASE, TP_CLEAR, TP_TRAVERSE};
67
use crate::pycell::impl_::PyClassBorrowChecker as _;
78
use crate::pycell::{PyBorrowError, PyBorrowMutError};
89
use crate::pyclass::boolean_struct::False;
910
use crate::types::any::PyAnyMethods;
1011
#[cfg(feature = "gil-refs")]
11-
use crate::types::{PyModule, PyType};
12+
use crate::types::PyModule;
13+
use crate::types::PyType;
1214
use crate::{
1315
ffi, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject, PyRef,
1416
PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python,
@@ -20,6 +22,8 @@ use std::os::raw::{c_int, c_void};
2022
use std::panic::{catch_unwind, AssertUnwindSafe};
2123
use std::ptr::null_mut;
2224

25+
use super::trampoline;
26+
2327
/// Python 3.8 and up - __ipow__ has modulo argument correctly populated.
2428
#[cfg(Py_3_8)]
2529
#[repr(transparent)]
@@ -277,6 +281,7 @@ pub unsafe fn _call_traverse<T>(
277281
impl_: fn(&T, PyVisit<'_>) -> Result<(), PyTraverseError>,
278282
visit: ffi::visitproc,
279283
arg: *mut c_void,
284+
current_traverse: ffi::traverseproc,
280285
) -> c_int
281286
where
282287
T: PyClass,
@@ -291,6 +296,11 @@ where
291296
let trap = PanicTrap::new("uncaught panic inside __traverse__ handler");
292297
let lock = LockGIL::during_traverse();
293298

299+
let super_retval = call_super_traverse(slf, visit, arg, current_traverse);
300+
if super_retval != 0 {
301+
return super_retval;
302+
}
303+
294304
// SAFETY: `slf` is a valid Python object pointer to a class object of type T, and
295305
// traversal is running so no mutations can occur.
296306
let class_object: &PyClassObject<T> = &*slf.cast();
@@ -330,6 +340,127 @@ where
330340
retval
331341
}
332342

343+
/// Call super-type traverse method, if necessary.
344+
///
345+
/// Adapted from <https://github.com/cython/cython/blob/7acfb375fb54a033f021b0982a3cd40c34fb22ac/Cython/Utility/ExtensionTypes.c#L386>
346+
///
347+
/// TODO: There are possible optimizations over looking up the base type in this way
348+
/// - if the base type is known in this module, can potentially look it up directly in module state
349+
/// (when we have it)
350+
/// - if the base type is a Python builtin, can jut call the C function directly
351+
/// - if the base type is a PyO3 type defined in the same module, can potentially do similar to
352+
/// tp_alloc where we solve this at compile time
353+
unsafe fn call_super_traverse(
354+
obj: *mut ffi::PyObject,
355+
visit: ffi::visitproc,
356+
arg: *mut c_void,
357+
current_traverse: ffi::traverseproc,
358+
) -> c_int {
359+
// SAFETY: in this function here it's ok to work with raw type objects `ffi::Py_TYPE`
360+
// because the GC is running and so
361+
// - (a) we cannot do refcounting and
362+
// - (b) the type of the object cannot change.
363+
let mut ty = ffi::Py_TYPE(obj);
364+
let mut traverse: Option<ffi::traverseproc>;
365+
366+
// First find the current type by the current_traverse function
367+
loop {
368+
traverse = get_slot(ty, TP_TRAVERSE);
369+
if traverse == Some(current_traverse) {
370+
break;
371+
}
372+
ty = get_slot(ty, TP_BASE);
373+
if ty.is_null() {
374+
// FIXME: return an error if current type not in the MRO? Should be impossible.
375+
return 0;
376+
}
377+
}
378+
379+
// Get first base which has a different traverse function
380+
while traverse == Some(current_traverse) {
381+
ty = get_slot(ty, TP_BASE);
382+
if ty.is_null() {
383+
break;
384+
}
385+
traverse = get_slot(ty, TP_TRAVERSE);
386+
}
387+
388+
// If we found a type with a different traverse function, call it
389+
if let Some(traverse) = traverse {
390+
return traverse(obj, visit, arg);
391+
}
392+
393+
// FIXME same question as cython: what if the current type is not in the MRO?
394+
0
395+
}
396+
397+
/// Calls an implementation of __clear__ for tp_clear
398+
pub unsafe fn _call_clear(
399+
slf: *mut ffi::PyObject,
400+
impl_: for<'py> unsafe fn(Python<'py>, *mut ffi::PyObject) -> PyResult<()>,
401+
current_clear: ffi::inquiry,
402+
) -> c_int {
403+
trampoline::trampoline(move |py| {
404+
let super_retval = call_super_clear(py, slf, current_clear);
405+
if super_retval != 0 {
406+
return Err(PyErr::fetch(py));
407+
}
408+
impl_(py, slf)?;
409+
Ok(0)
410+
})
411+
}
412+
413+
/// Call super-type traverse method, if necessary.
414+
///
415+
/// Adapted from <https://github.com/cython/cython/blob/7acfb375fb54a033f021b0982a3cd40c34fb22ac/Cython/Utility/ExtensionTypes.c#L386>
416+
///
417+
/// TODO: There are possible optimizations over looking up the base type in this way
418+
/// - if the base type is known in this module, can potentially look it up directly in module state
419+
/// (when we have it)
420+
/// - if the base type is a Python builtin, can jut call the C function directly
421+
/// - if the base type is a PyO3 type defined in the same module, can potentially do similar to
422+
/// tp_alloc where we solve this at compile time
423+
unsafe fn call_super_clear(
424+
py: Python<'_>,
425+
obj: *mut ffi::PyObject,
426+
current_clear: ffi::inquiry,
427+
) -> c_int {
428+
let mut ty = PyType::from_borrowed_type_ptr(py, ffi::Py_TYPE(obj));
429+
let mut clear: Option<ffi::inquiry>;
430+
431+
// First find the current type by the current_clear function
432+
loop {
433+
clear = ty.get_slot(TP_CLEAR);
434+
if clear == Some(current_clear) {
435+
break;
436+
}
437+
let base = ty.get_slot(TP_BASE);
438+
if base.is_null() {
439+
// FIXME: return an error if current type not in the MRO? Should be impossible.
440+
return 0;
441+
}
442+
ty = PyType::from_borrowed_type_ptr(py, base);
443+
}
444+
445+
// Get first base which has a different clear function
446+
while clear == Some(current_clear) {
447+
let base = ty.get_slot(TP_BASE);
448+
if base.is_null() {
449+
break;
450+
}
451+
ty = PyType::from_borrowed_type_ptr(py, base);
452+
clear = ty.get_slot(TP_CLEAR);
453+
}
454+
455+
// If we found a type with a different clear function, call it
456+
if let Some(clear) = clear {
457+
return clear(obj);
458+
}
459+
460+
// FIXME same question as cython: what if the current type is not in the MRO?
461+
0
462+
}
463+
333464
// Autoref-based specialization for handling `__next__` returning `Option`
334465

335466
pub struct IterBaseTag;

src/internal/get_slot.rs

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ impl Bound<'_, PyType> {
1111
where
1212
Slot<S>: GetSlotImpl,
1313
{
14-
slot.get_slot(self.as_borrowed())
14+
// SAFETY: `self` is a valid type object.
15+
unsafe {
16+
slot.get_slot(
17+
self.as_type_ptr(),
18+
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
19+
is_runtime_3_10(self.py()),
20+
)
21+
}
1522
}
1623
}
1724

@@ -21,13 +28,50 @@ impl Borrowed<'_, '_, PyType> {
2128
where
2229
Slot<S>: GetSlotImpl,
2330
{
24-
slot.get_slot(self)
31+
// SAFETY: `self` is a valid type object.
32+
unsafe {
33+
slot.get_slot(
34+
self.as_type_ptr(),
35+
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
36+
is_runtime_3_10(self.py()),
37+
)
38+
}
2539
}
2640
}
2741

42+
/// Gets a slot from a raw FFI pointer.
43+
///
44+
/// Safety:
45+
/// - `ty` must be a valid non-null pointer to a `PyTypeObject`.
46+
/// - The Python runtime must be initialized
47+
pub(crate) unsafe fn get_slot<const S: c_int>(
48+
ty: *mut ffi::PyTypeObject,
49+
slot: Slot<S>,
50+
) -> <Slot<S> as GetSlotImpl>::Type
51+
where
52+
Slot<S>: GetSlotImpl,
53+
{
54+
slot.get_slot(
55+
ty,
56+
// SAFETY: the Python runtime is initialized
57+
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
58+
is_runtime_3_10(crate::Python::assume_gil_acquired()),
59+
)
60+
}
61+
2862
pub(crate) trait GetSlotImpl {
2963
type Type;
30-
fn get_slot(self, tp: Borrowed<'_, '_, PyType>) -> Self::Type;
64+
65+
/// Gets the requested slot from a type object.
66+
///
67+
/// Safety:
68+
/// - `ty` must be a valid non-null pointer to a `PyTypeObject`.
69+
/// - `is_runtime_3_10` must be `false` if the runtime is not Python 3.10 or later.
70+
unsafe fn get_slot(
71+
self,
72+
ty: *mut ffi::PyTypeObject,
73+
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] is_runtime_3_10: bool,
74+
) -> Self::Type;
3175
}
3276

3377
#[derive(Copy, Clone)]
@@ -42,12 +86,14 @@ macro_rules! impl_slots {
4286
type Type = $tp;
4387

4488
#[inline]
45-
fn get_slot(self, tp: Borrowed<'_, '_, PyType>) -> Self::Type {
46-
let ptr = tp.as_type_ptr();
47-
89+
unsafe fn get_slot(
90+
self,
91+
ty: *mut ffi::PyTypeObject,
92+
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] is_runtime_3_10: bool
93+
) -> Self::Type {
4894
#[cfg(not(Py_LIMITED_API))]
49-
unsafe {
50-
(*ptr).$field
95+
{
96+
(*ty).$field
5197
}
5298

5399
#[cfg(Py_LIMITED_API)]
@@ -59,27 +105,29 @@ macro_rules! impl_slots {
59105
// (3.7, 3.8, 3.9) and then look in the type object anyway. This is only ok
60106
// because we know that the interpreter is not going to change the size
61107
// of the type objects for these historical versions.
62-
if !is_runtime_3_10(tp.py())
63-
&& unsafe { ffi::PyType_HasFeature(ptr, ffi::Py_TPFLAGS_HEAPTYPE) } == 0
108+
if !is_runtime_3_10 && ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) == 0
64109
{
65-
return unsafe { (*ptr.cast::<PyTypeObject39Snapshot>()).$field };
110+
return (*ty.cast::<PyTypeObject39Snapshot>()).$field;
66111
}
67112
}
68113

69114
// SAFETY: slot type is set carefully to be valid
70-
unsafe { std::mem::transmute(ffi::PyType_GetSlot(ptr, ffi::$slot)) }
115+
std::mem::transmute(ffi::PyType_GetSlot(ty, ffi::$slot))
71116
}
72117
}
73118
}
74119
)*
75120
};
76121
}
77122

78-
// Slots are implemented on-demand as needed.
123+
// Slots are implemented on-demand as needed.)
79124
impl_slots! {
80125
TP_ALLOC: (Py_tp_alloc, tp_alloc) -> Option<ffi::allocfunc>,
126+
TP_BASE: (Py_tp_base, tp_base) -> *mut ffi::PyTypeObject,
127+
TP_CLEAR: (Py_tp_clear, tp_clear) -> Option<ffi::inquiry>,
81128
TP_DESCR_GET: (Py_tp_descr_get, tp_descr_get) -> Option<ffi::descrgetfunc>,
82129
TP_FREE: (Py_tp_free, tp_free) -> Option<ffi::freefunc>,
130+
TP_TRAVERSE: (Py_tp_traverse, tp_traverse) -> Option<ffi::traverseproc>,
83131
}
84132

85133
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]

0 commit comments

Comments
 (0)