Skip to content

Commit f71fe9b

Browse files
authored
Add GC infrastructure: tracking bits, tp_clear (#6977)
GC bit operations (_PyObject_GC_TRACK/UNTRACK equivalent): - Add set_gc_bit() helper for atomic GC bit manipulation - Add set_gc_tracked() / clear_gc_tracked() methods - Update is_gc_tracked() to use GcBits::TRACKED flag - Call set_gc_tracked() in track_object() - Call clear_gc_tracked() for static types (they are immortal) tp_clear infrastructure (for breaking reference cycles): - Add try_clear_obj() function to call payload's try_clear - Add clear field to PyObjVTable - Add clear() method to PyType's Traverse impl
1 parent 023b3b2 commit f71fe9b

File tree

4 files changed

+114
-14
lines changed

4 files changed

+114
-14
lines changed

crates/vm/src/builtins/type.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,33 @@ unsafe impl crate::object::Traverse for PyType {
5757
.map(|(_, v)| v.traverse(tracer_fn))
5858
.count();
5959
}
60+
61+
/// type_clear: break reference cycles in type objects
62+
fn clear(&mut self, out: &mut Vec<crate::PyObjectRef>) {
63+
if let Some(base) = self.base.take() {
64+
out.push(base.into());
65+
}
66+
if let Some(mut guard) = self.bases.try_write() {
67+
for base in guard.drain(..) {
68+
out.push(base.into());
69+
}
70+
}
71+
if let Some(mut guard) = self.mro.try_write() {
72+
for typ in guard.drain(..) {
73+
out.push(typ.into());
74+
}
75+
}
76+
if let Some(mut guard) = self.subclasses.try_write() {
77+
for weak in guard.drain(..) {
78+
out.push(weak.into());
79+
}
80+
}
81+
if let Some(mut guard) = self.attributes.try_write() {
82+
for (_, val) in guard.drain(..) {
83+
out.push(val);
84+
}
85+
}
86+
}
6087
}
6188

6289
// PyHeapTypeObject in CPython
@@ -393,6 +420,11 @@ impl PyType {
393420
metaclass,
394421
None,
395422
);
423+
424+
// Static types are not tracked by GC.
425+
// They are immortal and never participate in collectable cycles.
426+
new_type.as_object().clear_gc_tracked();
427+
396428
new_type.mro.write().insert(0, new_type.clone());
397429

398430
// Note: inherit_slots is called in PyClassImpl::init_class after

crates/vm/src/gc_state.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ impl GcState {
236236
pub unsafe fn track_object(&self, obj: NonNull<PyObject>) {
237237
let gc_ptr = GcObjectPtr(obj);
238238

239+
// _PyObject_GC_TRACK
240+
let obj_ref = unsafe { obj.as_ref() };
241+
obj_ref.set_gc_tracked();
242+
239243
// Add to generation 0 tracking first (for correct gc_refs algorithm)
240244
// Only increment count if we successfully add to the set
241245
if let Ok(mut gen0) = self.generation_objects[0].write()

crates/vm/src/object/core.rs

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,28 @@ use core::{
8181
#[derive(Debug)]
8282
pub(super) struct Erased;
8383

84-
/// Default dealloc: handles __del__, weakref clearing, and memory free.
84+
/// Default dealloc: handles __del__, weakref clearing, tp_clear, and memory free.
8585
/// Equivalent to subtype_dealloc in CPython.
8686
pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
8787
let obj_ref = unsafe { &*(obj as *const PyObject) };
8888
if let Err(()) = obj_ref.drop_slow_inner() {
8989
return; // resurrected by __del__
9090
}
91+
92+
// Extract child references before deallocation to break circular refs (tp_clear).
93+
// This ensures that when edges are dropped after the object is freed,
94+
// any pointers back to this object are already gone.
95+
let mut edges = Vec::new();
96+
if let Some(clear_fn) = obj_ref.0.vtable.clear {
97+
unsafe { clear_fn(obj, &mut edges) };
98+
}
99+
100+
// Deallocate the object memory
91101
drop(unsafe { Box::from_raw(obj as *mut PyInner<T>) });
102+
103+
// Drop child references - may trigger recursive destruction.
104+
// The object is already deallocated, so circular refs are broken.
105+
drop(edges);
92106
}
93107
pub(super) unsafe fn debug_obj<T: PyPayload + core::fmt::Debug>(
94108
x: &PyObject,
@@ -105,6 +119,12 @@ pub(super) unsafe fn try_traverse_obj<T: PyPayload>(x: &PyObject, tracer_fn: &mu
105119
payload.try_traverse(tracer_fn)
106120
}
107121

122+
/// Call `try_clear` on payload to extract child references (tp_clear)
123+
pub(super) unsafe fn try_clear_obj<T: PyPayload>(x: *mut PyObject, out: &mut Vec<PyObjectRef>) {
124+
let x = unsafe { &mut *(x as *mut PyInner<T>) };
125+
x.payload.try_clear(out);
126+
}
127+
108128
bitflags::bitflags! {
109129
/// GC bits for free-threading support (like ob_gc_bits in Py_GIL_DISABLED)
110130
/// These bits are stored in a separate atomic field for lock-free access.
@@ -963,10 +983,27 @@ impl PyObject {
963983
/// _PyGC_SET_FINALIZED in Py_GIL_DISABLED mode.
964984
#[inline]
965985
fn set_gc_finalized(&self) {
966-
// Atomic RMW to avoid clobbering other concurrent bit updates.
986+
self.set_gc_bit(GcBits::FINALIZED);
987+
}
988+
989+
/// Set a GC bit atomically.
990+
#[inline]
991+
pub(crate) fn set_gc_bit(&self, bit: GcBits) {
992+
self.0.gc_bits.fetch_or(bit.bits(), Ordering::Relaxed);
993+
}
994+
995+
/// _PyObject_GC_TRACK
996+
#[inline]
997+
pub(crate) fn set_gc_tracked(&self) {
998+
self.set_gc_bit(GcBits::TRACKED);
999+
}
1000+
1001+
/// _PyObject_GC_UNTRACK
1002+
#[inline]
1003+
pub(crate) fn clear_gc_tracked(&self) {
9671004
self.0
9681005
.gc_bits
969-
.fetch_or(GcBits::FINALIZED.bits(), Ordering::Relaxed);
1006+
.fetch_and(!GcBits::TRACKED.bits(), Ordering::Relaxed);
9701007
}
9711008

9721009
#[inline(always)] // the outer function is never inlined
@@ -1046,13 +1083,9 @@ impl PyObject {
10461083
*self.0.slots[offset].write() = value;
10471084
}
10481085

1049-
/// Check if this object is tracked by the garbage collector.
1050-
/// Returns true if the object has a trace function or has an instance dict.
1086+
/// _PyObject_GC_IS_TRACKED
10511087
pub fn is_gc_tracked(&self) -> bool {
1052-
if self.0.vtable.trace.is_some() {
1053-
return true;
1054-
}
1055-
self.0.dict.is_some()
1088+
GcBits::from_bits_retain(self.0.gc_bits.load(Ordering::Relaxed)).contains(GcBits::TRACKED)
10561089
}
10571090

10581091
/// Get the referents (objects directly referenced) of this object.
@@ -1277,13 +1310,28 @@ impl<T: PyPayload> PyRef<T> {
12771310
}
12781311
}
12791312

1280-
impl<T: PyPayload + core::fmt::Debug> PyRef<T> {
1313+
impl<T: PyPayload + crate::object::MaybeTraverse + core::fmt::Debug> PyRef<T> {
12811314
#[inline(always)]
12821315
pub fn new_ref(payload: T, typ: crate::builtins::PyTypeRef, dict: Option<PyDictRef>) -> Self {
1316+
let has_dict = dict.is_some();
1317+
let is_heaptype = typ.heaptype_ext.is_some();
12831318
let inner = Box::into_raw(PyInner::new(payload, typ, dict));
1284-
Self {
1285-
ptr: unsafe { NonNull::new_unchecked(inner.cast::<Py<T>>()) },
1319+
let ptr = unsafe { NonNull::new_unchecked(inner.cast::<Py<T>>()) };
1320+
1321+
// Track object if:
1322+
// - HAS_TRAVERSE is true (Rust payload implements Traverse), OR
1323+
// - has instance dict (user-defined class instances), OR
1324+
// - heap type (all heap type instances are GC-tracked, like Py_TPFLAGS_HAVE_GC)
1325+
if <T as crate::object::MaybeTraverse>::HAS_TRAVERSE || has_dict || is_heaptype {
1326+
let gc = crate::gc_state::gc_state();
1327+
unsafe {
1328+
gc.track_object(ptr.cast());
1329+
}
1330+
// Check if automatic GC should run
1331+
gc.maybe_collect();
12861332
}
1333+
1334+
Self { ptr }
12871335
}
12881336
}
12891337

@@ -1546,6 +1594,12 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) {
15461594
heaptype_ext: None,
15471595
};
15481596
let weakref_type = PyRef::new_ref(weakref_type, type_type.clone(), None);
1597+
// Static type: untrack from GC (was tracked by new_ref because PyType has HAS_TRAVERSE)
1598+
unsafe {
1599+
crate::gc_state::gc_state()
1600+
.untrack_object(core::ptr::NonNull::from(weakref_type.as_object()));
1601+
}
1602+
weakref_type.as_object().clear_gc_tracked();
15491603
// weakref's mro is [weakref, object]
15501604
weakref_type.mro.write().insert(0, weakref_type.clone());
15511605

crates/vm/src/object/traverse_object.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ use alloc::fmt;
22
use core::any::TypeId;
33

44
use crate::{
5-
PyObject,
5+
PyObject, PyObjectRef,
66
object::{
77
Erased, InstanceDict, MaybeTraverse, PyInner, PyObjectPayload, debug_obj, default_dealloc,
8-
try_traverse_obj,
8+
try_clear_obj, try_traverse_obj,
99
},
1010
};
1111

@@ -17,6 +17,9 @@ pub(in crate::object) struct PyObjVTable {
1717
pub(in crate::object) dealloc: unsafe fn(*mut PyObject),
1818
pub(in crate::object) debug: unsafe fn(&PyObject, &mut fmt::Formatter<'_>) -> fmt::Result,
1919
pub(in crate::object) trace: Option<unsafe fn(&PyObject, &mut TraverseFn<'_>)>,
20+
/// Clear for circular reference resolution (tp_clear).
21+
/// Called just before deallocation to extract child references.
22+
pub(in crate::object) clear: Option<unsafe fn(*mut PyObject, &mut Vec<PyObjectRef>)>,
2023
}
2124

2225
impl PyObjVTable {
@@ -32,6 +35,13 @@ impl PyObjVTable {
3235
None
3336
}
3437
},
38+
clear: const {
39+
if T::HAS_CLEAR {
40+
Some(try_clear_obj::<T>)
41+
} else {
42+
None
43+
}
44+
},
3545
}
3646
}
3747
}

0 commit comments

Comments
 (0)