mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
Replace GC tracking HashSet with intrusive linked list (#7328)
* Replace GC tracking HashSet with intrusive linked list Replace per-generation HashSet<GcObjectPtr> with intrusive doubly-linked lists for GC object tracking. Each PyInner now carries gc_pointers (prev/next) and gc_generation fields, enabling O(1) track/untrack without hashing. - Add gc_pointers (Pointers<PyObject>) and gc_generation (u8) to PyInner - Implement GcLink trait for intrusive list integration - Replace generation_objects/permanent_objects/tracked_objects/finalized_objects HashSets with generation_lists/permanent_list LinkedLists - Use GcBits::FINALIZED flag instead of finalized_objects HashSet - Change default_dealloc to untrack directly before memory free - Hold both src/dst list locks in promote_survivors to prevent race conditions with concurrent untrack_object calls - Add pop_front to LinkedList for freeze/unfreeze operations Move unreachable_refs creation before drop(gen_locks) so that raw pointer dereferences and refcount increments happen while generation list read locks are held. Previously, after dropping read locks, other threads could untrack and free objects, causing use-after-free when creating strong references from the raw GcPtr pointers.
This commit is contained in:
@@ -157,6 +157,15 @@ impl<L: Link> LinkedList<L, L::Target> {
|
||||
let ptr = L::as_raw(&val);
|
||||
assert_ne!(self.head, Some(ptr));
|
||||
unsafe {
|
||||
// Verify the node is not already in a list (pointers must be clean)
|
||||
debug_assert!(
|
||||
L::pointers(ptr).as_ref().get_prev().is_none(),
|
||||
"push_front: node already has prev pointer (double-insert?)"
|
||||
);
|
||||
debug_assert!(
|
||||
L::pointers(ptr).as_ref().get_next().is_none(),
|
||||
"push_front: node already has next pointer (double-insert?)"
|
||||
);
|
||||
L::pointers(ptr).as_mut().set_next(self.head);
|
||||
L::pointers(ptr).as_mut().set_prev(None);
|
||||
|
||||
@@ -192,6 +201,20 @@ impl<L: Link> LinkedList<L, L::Target> {
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Removes the first element from the list and returns it, or None if empty.
|
||||
pub fn pop_front(&mut self) -> Option<L::Handle> {
|
||||
let head = self.head?;
|
||||
unsafe {
|
||||
self.head = L::pointers(head).as_ref().get_next();
|
||||
if let Some(new_head) = self.head {
|
||||
L::pointers(new_head).as_mut().set_prev(None);
|
||||
}
|
||||
L::pointers(head).as_mut().set_next(None);
|
||||
L::pointers(head).as_mut().set_prev(None);
|
||||
Some(L::from_raw(head))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the linked list does not contain any node
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
self.head.is_none()
|
||||
@@ -212,7 +235,11 @@ impl<L: Link> LinkedList<L, L::Target> {
|
||||
pub unsafe fn remove(&mut self, node: NonNull<L::Target>) -> Option<L::Handle> {
|
||||
unsafe {
|
||||
if let Some(prev) = L::pointers(node).as_ref().get_prev() {
|
||||
debug_assert_eq!(L::pointers(prev).as_ref().get_next(), Some(node));
|
||||
debug_assert_eq!(
|
||||
L::pointers(prev).as_ref().get_next(),
|
||||
Some(node),
|
||||
"linked list corruption: prev->next != node (prev={prev:p}, node={node:p})"
|
||||
);
|
||||
L::pointers(prev)
|
||||
.as_mut()
|
||||
.set_next(L::pointers(node).as_ref().get_next());
|
||||
@@ -225,7 +252,11 @@ impl<L: Link> LinkedList<L, L::Target> {
|
||||
}
|
||||
|
||||
if let Some(next) = L::pointers(node).as_ref().get_next() {
|
||||
debug_assert_eq!(L::pointers(next).as_ref().get_prev(), Some(node));
|
||||
debug_assert_eq!(
|
||||
L::pointers(next).as_ref().get_prev(),
|
||||
Some(node),
|
||||
"linked list corruption: next->prev != node (next={next:p}, node={node:p})"
|
||||
);
|
||||
L::pointers(next)
|
||||
.as_mut()
|
||||
.set_prev(L::pointers(node).as_ref().get_prev());
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
//! This module implements CPython-compatible generational garbage collection
|
||||
//! for RustPython, using an intrusive doubly-linked list approach.
|
||||
|
||||
use crate::common::linked_list::LinkedList;
|
||||
use crate::common::lock::{PyMutex, PyRwLock};
|
||||
use crate::object::{GC_PERMANENT, GC_UNTRACKED, GcLink};
|
||||
use crate::{AsObject, PyObject, PyObjectRef};
|
||||
use core::ptr::NonNull;
|
||||
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
|
||||
@@ -96,13 +98,10 @@ impl GcGeneration {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for raw pointer to make it Send + Sync
|
||||
/// Wrapper for NonNull<PyObject> to impl Hash/Eq for use in temporary collection sets.
|
||||
/// Only used within collect_inner, never shared across threads.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
struct GcObjectPtr(NonNull<PyObject>);
|
||||
|
||||
// SAFETY: We only use this for tracking objects, and proper synchronization is used
|
||||
unsafe impl Send for GcObjectPtr {}
|
||||
unsafe impl Sync for GcObjectPtr {}
|
||||
struct GcPtr(NonNull<PyObject>);
|
||||
|
||||
/// Global GC state
|
||||
pub struct GcState {
|
||||
@@ -112,11 +111,11 @@ pub struct GcState {
|
||||
pub permanent: GcGeneration,
|
||||
/// GC enabled flag
|
||||
pub enabled: AtomicBool,
|
||||
/// Per-generation object tracking (for correct gc_refs algorithm)
|
||||
/// Objects start in gen0, survivors move to gen1, then gen2
|
||||
generation_objects: [PyRwLock<HashSet<GcObjectPtr>>; 3],
|
||||
/// Per-generation intrusive linked lists for object tracking.
|
||||
/// Objects start in gen0, survivors are promoted to gen1, then gen2.
|
||||
generation_lists: [PyRwLock<LinkedList<GcLink, PyObject>>; 3],
|
||||
/// Frozen/permanent objects (excluded from normal GC)
|
||||
permanent_objects: PyRwLock<HashSet<GcObjectPtr>>,
|
||||
permanent_list: PyRwLock<LinkedList<GcLink, PyObject>>,
|
||||
/// Debug flags
|
||||
pub debug: AtomicU32,
|
||||
/// gc.garbage list (uncollectable objects with __del__)
|
||||
@@ -127,17 +126,10 @@ pub struct GcState {
|
||||
collecting: PyMutex<()>,
|
||||
/// Allocation counter for gen0
|
||||
alloc_count: AtomicUsize,
|
||||
/// Registry of all tracked objects (for cycle detection)
|
||||
tracked_objects: PyRwLock<HashSet<GcObjectPtr>>,
|
||||
/// Objects that have been finalized (__del__ already called)
|
||||
/// Prevents calling __del__ multiple times on resurrected objects
|
||||
finalized_objects: PyRwLock<HashSet<GcObjectPtr>>,
|
||||
}
|
||||
|
||||
// SAFETY: GcObjectPtr wraps NonNull which is !Send/!Sync, but we only use it as an opaque
|
||||
// hash key. PyObjectRef is Send. In threading mode, PyRwLock/PyMutex are parking_lot based
|
||||
// and genuinely Sync. In non-threading mode, gc_state() is thread-local so neither Send
|
||||
// nor Sync is needed.
|
||||
// SAFETY: All fields are either inherently Send/Sync (atomics, RwLock, Mutex) or protected by PyMutex.
|
||||
// LinkedList<GcLink, PyObject> is Send+Sync because GcLink's Target (PyObject) is Send+Sync.
|
||||
#[cfg(feature = "threading")]
|
||||
unsafe impl Send for GcState {}
|
||||
#[cfg(feature = "threading")]
|
||||
@@ -159,19 +151,17 @@ impl GcState {
|
||||
],
|
||||
permanent: GcGeneration::new(0),
|
||||
enabled: AtomicBool::new(true),
|
||||
generation_objects: [
|
||||
PyRwLock::new(HashSet::new()),
|
||||
PyRwLock::new(HashSet::new()),
|
||||
PyRwLock::new(HashSet::new()),
|
||||
generation_lists: [
|
||||
PyRwLock::new(LinkedList::new()),
|
||||
PyRwLock::new(LinkedList::new()),
|
||||
PyRwLock::new(LinkedList::new()),
|
||||
],
|
||||
permanent_objects: PyRwLock::new(HashSet::new()),
|
||||
permanent_list: PyRwLock::new(LinkedList::new()),
|
||||
debug: AtomicU32::new(0),
|
||||
garbage: PyMutex::new(Vec::new()),
|
||||
callbacks: PyMutex::new(Vec::new()),
|
||||
collecting: PyMutex::new(()),
|
||||
alloc_count: AtomicUsize::new(0),
|
||||
tracked_objects: PyRwLock::new(HashSet::new()),
|
||||
finalized_objects: PyRwLock::new(HashSet::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,120 +228,99 @@ impl GcState {
|
||||
]
|
||||
}
|
||||
|
||||
/// Track a new object (add to gen0)
|
||||
/// Called when IS_TRACE objects are created
|
||||
/// Track a new object (add to gen0).
|
||||
/// O(1) — intrusive linked list push_front, no hashing.
|
||||
///
|
||||
/// # Safety
|
||||
/// obj must be a valid pointer to a PyObject
|
||||
pub unsafe fn track_object(&self, obj: NonNull<PyObject>) {
|
||||
let gc_ptr = GcObjectPtr(obj);
|
||||
|
||||
// _PyObject_GC_TRACK
|
||||
let obj_ref = unsafe { obj.as_ref() };
|
||||
obj_ref.set_gc_tracked();
|
||||
obj_ref.set_gc_generation(0);
|
||||
|
||||
// Add to generation 0 tracking first (for correct gc_refs algorithm)
|
||||
// Only increment count if we successfully add to the set
|
||||
{
|
||||
let mut gen0 = self.generation_objects[0].write();
|
||||
if gen0.insert(gc_ptr) {
|
||||
self.generations[0].count.fetch_add(1, Ordering::SeqCst);
|
||||
self.alloc_count.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
// Also add to global tracking (for get_objects, etc.)
|
||||
self.tracked_objects.write().insert(gc_ptr);
|
||||
self.generation_lists[0].write().push_front(obj);
|
||||
self.generations[0].count.fetch_add(1, Ordering::SeqCst);
|
||||
self.alloc_count.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Untrack an object (remove from GC lists)
|
||||
/// Called when objects are deallocated
|
||||
/// Untrack an object (remove from GC lists).
|
||||
/// O(1) — intrusive linked list remove by node pointer.
|
||||
///
|
||||
/// # Safety
|
||||
/// obj must be a valid pointer to a PyObject
|
||||
/// obj must be a valid pointer to a PyObject that is currently tracked.
|
||||
/// The object's memory must still be valid (pointers are read).
|
||||
pub unsafe fn untrack_object(&self, obj: NonNull<PyObject>) {
|
||||
let gc_ptr = GcObjectPtr(obj);
|
||||
let obj_ref = unsafe { obj.as_ref() };
|
||||
|
||||
// Remove from generation tracking lists and decrement the correct generation's count
|
||||
for (gen_idx, generation) in self.generation_objects.iter().enumerate() {
|
||||
let mut gen_set = generation.write();
|
||||
if gen_set.remove(&gc_ptr) {
|
||||
// Decrement count for the generation we removed from
|
||||
let count = self.generations[gen_idx].count.load(Ordering::SeqCst);
|
||||
if count > 0 {
|
||||
self.generations[gen_idx]
|
||||
.count
|
||||
.fetch_sub(1, Ordering::SeqCst);
|
||||
}
|
||||
break; // Object can only be in one generation
|
||||
loop {
|
||||
let obj_gen = obj_ref.gc_generation();
|
||||
|
||||
let (list_lock, count) = if obj_gen <= 2 {
|
||||
(
|
||||
&self.generation_lists[obj_gen as usize]
|
||||
as &PyRwLock<LinkedList<GcLink, PyObject>>,
|
||||
&self.generations[obj_gen as usize].count,
|
||||
)
|
||||
} else if obj_gen == GC_PERMANENT {
|
||||
(&self.permanent_list, &self.permanent.count)
|
||||
} else {
|
||||
return; // GC_UNTRACKED or unknown — already untracked
|
||||
};
|
||||
|
||||
let mut list = list_lock.write();
|
||||
// Re-check generation under lock (may have changed due to promotion)
|
||||
if obj_ref.gc_generation() != obj_gen {
|
||||
drop(list);
|
||||
continue; // Retry with the updated generation
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from global tracking
|
||||
self.tracked_objects.write().remove(&gc_ptr);
|
||||
|
||||
// Remove from permanent tracking
|
||||
{
|
||||
let mut permanent = self.permanent_objects.write();
|
||||
if permanent.remove(&gc_ptr) {
|
||||
let count = self.permanent.count.load(Ordering::SeqCst);
|
||||
if count > 0 {
|
||||
self.permanent.count.fetch_sub(1, Ordering::SeqCst);
|
||||
}
|
||||
if unsafe { list.remove(obj) }.is_some() {
|
||||
count.fetch_sub(1, Ordering::SeqCst);
|
||||
obj_ref.clear_gc_tracked();
|
||||
obj_ref.set_gc_generation(GC_UNTRACKED);
|
||||
} else {
|
||||
// Object claims to be in this generation but wasn't found in the list.
|
||||
// This indicates a bug: the object was already removed from the list
|
||||
// without updating gc_generation, or was never inserted.
|
||||
eprintln!(
|
||||
"GC WARNING: untrack_object failed to remove obj={obj:p} from gen={obj_gen}, \
|
||||
tracked={}, gc_gen={}",
|
||||
obj_ref.is_gc_tracked(),
|
||||
obj_ref.gc_generation()
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from finalized set
|
||||
self.finalized_objects.write().remove(&gc_ptr);
|
||||
}
|
||||
|
||||
/// Check if an object has been finalized
|
||||
pub fn is_finalized(&self, obj: NonNull<PyObject>) -> bool {
|
||||
let gc_ptr = GcObjectPtr(obj);
|
||||
self.finalized_objects.read().contains(&gc_ptr)
|
||||
}
|
||||
|
||||
/// Mark an object as finalized
|
||||
pub fn mark_finalized(&self, obj: NonNull<PyObject>) {
|
||||
let gc_ptr = GcObjectPtr(obj);
|
||||
self.finalized_objects.write().insert(gc_ptr);
|
||||
}
|
||||
|
||||
/// Get tracked objects (for gc.get_objects)
|
||||
/// If generation is None, returns all tracked objects.
|
||||
/// If generation is Some(n), returns objects in generation n only.
|
||||
pub fn get_objects(&self, generation: Option<i32>) -> Vec<PyObjectRef> {
|
||||
fn collect_from_list(
|
||||
list: &LinkedList<GcLink, PyObject>,
|
||||
) -> impl Iterator<Item = PyObjectRef> + '_ {
|
||||
list.iter().filter_map(|obj| {
|
||||
if obj.strong_count() > 0 {
|
||||
Some(obj.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
match generation {
|
||||
None => {
|
||||
// Return all tracked objects
|
||||
self.tracked_objects
|
||||
.read()
|
||||
.iter()
|
||||
.filter_map(|ptr| {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
if obj.strong_count() > 0 {
|
||||
Some(obj.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
// Return all tracked objects from all generations + permanent
|
||||
let mut result = Vec::new();
|
||||
for gen_list in &self.generation_lists {
|
||||
result.extend(collect_from_list(&gen_list.read()));
|
||||
}
|
||||
result.extend(collect_from_list(&self.permanent_list.read()));
|
||||
result
|
||||
}
|
||||
Some(g) if (0..=2).contains(&g) => {
|
||||
// Return objects in specific generation
|
||||
let gen_idx = g as usize;
|
||||
self.generation_objects[gen_idx]
|
||||
.read()
|
||||
.iter()
|
||||
.filter_map(|ptr| {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
if obj.strong_count() > 0 {
|
||||
Some(obj.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
let guard = self.generation_lists[g as usize].read();
|
||||
collect_from_list(&guard).collect()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
@@ -365,8 +334,6 @@ impl GcState {
|
||||
return false;
|
||||
}
|
||||
|
||||
// _PyObject_GC_Alloc checks thresholds
|
||||
|
||||
// Check gen0 threshold
|
||||
let count0 = self.generations[0].count.load(Ordering::SeqCst) as u32;
|
||||
let threshold0 = self.generations[0].threshold();
|
||||
@@ -379,14 +346,6 @@ impl GcState {
|
||||
}
|
||||
|
||||
/// Perform garbage collection on the given generation
|
||||
/// Returns (collected_count, uncollectable_count)
|
||||
///
|
||||
/// Implements CPython-compatible generational GC algorithm:
|
||||
/// - Only collects objects from generations 0 to `generation`
|
||||
/// - Uses gc_refs algorithm: gc_refs = strong_count - internal_refs
|
||||
/// - Only subtracts references between objects IN THE SAME COLLECTION
|
||||
///
|
||||
/// If `force` is true, collection runs even if GC is disabled (for manual gc.collect() calls)
|
||||
pub fn collect(&self, generation: usize) -> (usize, usize) {
|
||||
self.collect_inner(generation, false)
|
||||
}
|
||||
@@ -418,24 +377,21 @@ impl GcState {
|
||||
crate::builtins::type_::type_cache_clear();
|
||||
|
||||
// Step 1: Gather objects from generations 0..=generation
|
||||
// Hold read locks for the entire collection to prevent other threads
|
||||
// from untracking objects while we're iterating.
|
||||
// Hold read locks for the entire scan to prevent concurrent modifications.
|
||||
let gen_locks: Vec<_> = (0..=generation)
|
||||
.map(|i| self.generation_objects[i].read())
|
||||
.map(|i| self.generation_lists[i].read())
|
||||
.collect();
|
||||
|
||||
let mut collecting: HashSet<GcObjectPtr> = HashSet::new();
|
||||
for gen_set in &gen_locks {
|
||||
for &ptr in gen_set.iter() {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
let mut collecting: HashSet<GcPtr> = HashSet::new();
|
||||
for gen_list in &gen_locks {
|
||||
for obj in gen_list.iter() {
|
||||
if obj.strong_count() > 0 {
|
||||
collecting.insert(ptr);
|
||||
collecting.insert(GcPtr(NonNull::from(obj)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if collecting.is_empty() {
|
||||
// Reset gen0 count even if nothing to collect
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
self.generations[generation].update_stats(0, 0);
|
||||
return (0, 0);
|
||||
@@ -450,25 +406,21 @@ impl GcState {
|
||||
}
|
||||
|
||||
// Step 2: Build gc_refs map (copy reference counts)
|
||||
let mut gc_refs: std::collections::HashMap<GcObjectPtr, usize> =
|
||||
std::collections::HashMap::new();
|
||||
let mut gc_refs: std::collections::HashMap<GcPtr, usize> = std::collections::HashMap::new();
|
||||
for &ptr in &collecting {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
gc_refs.insert(ptr, obj.strong_count());
|
||||
}
|
||||
|
||||
// Step 3: Subtract internal references
|
||||
// CRITICAL: Only subtract refs to objects IN THE COLLECTING SET
|
||||
for &ptr in &collecting {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
// Double-check object is still alive
|
||||
if obj.strong_count() == 0 {
|
||||
continue;
|
||||
}
|
||||
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
|
||||
for child_ptr in referent_ptrs {
|
||||
let gc_ptr = GcObjectPtr(child_ptr);
|
||||
// Only decrement if child is also in the collecting set!
|
||||
let gc_ptr = GcPtr(child_ptr);
|
||||
if collecting.contains(&gc_ptr)
|
||||
&& let Some(refs) = gc_refs.get_mut(&gc_ptr)
|
||||
{
|
||||
@@ -478,12 +430,9 @@ impl GcState {
|
||||
}
|
||||
|
||||
// Step 4: Find reachable objects (gc_refs > 0) and traverse from them
|
||||
// Objects with gc_refs > 0 are definitely reachable from outside.
|
||||
// We need to mark all objects reachable from them as also reachable.
|
||||
let mut reachable: HashSet<GcObjectPtr> = HashSet::new();
|
||||
let mut worklist: Vec<GcObjectPtr> = Vec::new();
|
||||
let mut reachable: HashSet<GcPtr> = HashSet::new();
|
||||
let mut worklist: Vec<GcPtr> = Vec::new();
|
||||
|
||||
// Start with objects that have gc_refs > 0
|
||||
for (&ptr, &refs) in &gc_refs {
|
||||
if refs > 0 {
|
||||
reachable.insert(ptr);
|
||||
@@ -491,14 +440,12 @@ impl GcState {
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse reachable objects to find more reachable ones
|
||||
while let Some(ptr) = worklist.pop() {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
if obj.is_gc_tracked() {
|
||||
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
|
||||
for child_ptr in referent_ptrs {
|
||||
let gc_ptr = GcObjectPtr(child_ptr);
|
||||
// If child is in collecting set and not yet marked reachable
|
||||
let gc_ptr = GcPtr(child_ptr);
|
||||
if collecting.contains(&gc_ptr) && reachable.insert(gc_ptr) {
|
||||
worklist.push(gc_ptr);
|
||||
}
|
||||
@@ -506,8 +453,8 @@ impl GcState {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Find unreachable objects (in collecting but not in reachable)
|
||||
let unreachable: Vec<GcObjectPtr> = collecting.difference(&reachable).copied().collect();
|
||||
// Step 5: Find unreachable objects
|
||||
let unreachable: Vec<GcPtr> = collecting.difference(&reachable).copied().collect();
|
||||
|
||||
if debug.contains(GcDebugFlags::STATS) {
|
||||
eprintln!(
|
||||
@@ -517,23 +464,22 @@ impl GcState {
|
||||
);
|
||||
}
|
||||
|
||||
if unreachable.is_empty() {
|
||||
// No cycles found - promote survivors to next generation
|
||||
drop(gen_locks); // Release read locks before promoting
|
||||
self.promote_survivors(generation, &collecting);
|
||||
// Reset gen0 count
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
self.generations[generation].update_stats(0, 0);
|
||||
return (0, 0);
|
||||
}
|
||||
// Create strong references while read locks are still held.
|
||||
// After dropping gen_locks, other threads can untrack+free objects,
|
||||
// making the raw pointers in `reachable`/`unreachable` dangling.
|
||||
// Strong refs keep objects alive for later phases.
|
||||
let survivor_refs: Vec<PyObjectRef> = reachable
|
||||
.iter()
|
||||
.filter_map(|ptr| {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
if obj.strong_count() > 0 {
|
||||
Some(obj.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Release read locks before finalization phase.
|
||||
// This allows other threads to untrack objects while we finalize.
|
||||
drop(gen_locks);
|
||||
|
||||
// Step 6: Finalize unreachable objects and handle resurrection
|
||||
|
||||
// 6a: Get references to all unreachable objects
|
||||
let unreachable_refs: Vec<crate::PyObjectRef> = unreachable
|
||||
.iter()
|
||||
.filter_map(|ptr| {
|
||||
@@ -546,114 +492,98 @@ impl GcState {
|
||||
})
|
||||
.collect();
|
||||
|
||||
if unreachable.is_empty() {
|
||||
drop(gen_locks);
|
||||
self.promote_survivors(generation, &survivor_refs);
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
self.generations[generation].update_stats(0, 0);
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// Release read locks before finalization phase.
|
||||
drop(gen_locks);
|
||||
|
||||
// Step 6: Finalize unreachable objects and handle resurrection
|
||||
|
||||
if unreachable_refs.is_empty() {
|
||||
self.promote_survivors(generation, &reachable);
|
||||
// Reset gen0 count
|
||||
self.promote_survivors(generation, &survivor_refs);
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
self.generations[generation].update_stats(0, 0);
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// 6b: Record initial strong counts (for resurrection detection)
|
||||
// Each object has +1 from unreachable_refs, so initial count includes that
|
||||
let initial_counts: std::collections::HashMap<GcObjectPtr, usize> = unreachable_refs
|
||||
let initial_counts: std::collections::HashMap<GcPtr, usize> = unreachable_refs
|
||||
.iter()
|
||||
.map(|obj| {
|
||||
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj.as_ref()));
|
||||
let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
|
||||
(ptr, obj.strong_count())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 6c: Clear existing weakrefs BEFORE calling __del__
|
||||
// This invalidates existing weakrefs, but new weakrefs created during __del__
|
||||
// will still work (WeakRefList::add restores inner.obj if cleared)
|
||||
//
|
||||
// CRITICAL: We use a two-phase approach to match CPython behavior:
|
||||
// Phase 1: Clear ALL weakrefs (set inner.obj = None) and collect callbacks
|
||||
// Phase 2: Invoke ALL callbacks
|
||||
// This ensures that when a callback runs, ALL weakrefs to unreachable objects
|
||||
// are already dead (return None when called).
|
||||
let mut all_callbacks: Vec<(crate::PyRef<crate::object::PyWeak>, crate::PyObjectRef)> =
|
||||
Vec::new();
|
||||
for obj_ref in &unreachable_refs {
|
||||
let callbacks = obj_ref.gc_clear_weakrefs_collect_callbacks();
|
||||
all_callbacks.extend(callbacks);
|
||||
}
|
||||
// Phase 2: Now call all callbacks - at this point ALL weakrefs are cleared
|
||||
for (wr, cb) in all_callbacks {
|
||||
if let Some(Err(e)) = crate::vm::thread::with_vm(&cb, |vm| cb.call((wr.clone(),), vm)) {
|
||||
// Report the exception via run_unraisable
|
||||
crate::vm::thread::with_vm(&cb, |vm| {
|
||||
vm.run_unraisable(e.clone(), Some("weakref callback".to_owned()), cb.clone());
|
||||
});
|
||||
}
|
||||
// If with_vm returns None, we silently skip - no VM available to handle errors
|
||||
}
|
||||
|
||||
// 6d: Call __del__ on all unreachable objects
|
||||
// This allows resurrection to work correctly
|
||||
// Skip objects that have already been finalized (prevents multiple __del__ calls)
|
||||
// 6d: Call __del__ on unreachable objects (skip already-finalized).
|
||||
// try_call_finalizer() internally checks gc_finalized() and sets it,
|
||||
// so we must NOT set it beforehand.
|
||||
for obj_ref in &unreachable_refs {
|
||||
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj_ref.as_ref()));
|
||||
let already_finalized = self.finalized_objects.read().contains(&ptr);
|
||||
|
||||
if !already_finalized {
|
||||
// Mark as finalized BEFORE calling __del__
|
||||
// This ensures is_finalized() returns True inside __del__
|
||||
self.finalized_objects.write().insert(ptr);
|
||||
obj_ref.try_call_finalizer();
|
||||
}
|
||||
obj_ref.try_call_finalizer();
|
||||
}
|
||||
|
||||
// 6d: Detect resurrection - strong_count increased means object was resurrected
|
||||
// Step 1: Find directly resurrected objects (strong_count increased)
|
||||
let mut resurrected_set: HashSet<GcObjectPtr> = HashSet::new();
|
||||
let unreachable_set: HashSet<GcObjectPtr> = unreachable.iter().copied().collect();
|
||||
// Detect resurrection
|
||||
let mut resurrected_set: HashSet<GcPtr> = HashSet::new();
|
||||
let unreachable_set: HashSet<GcPtr> = unreachable.iter().copied().collect();
|
||||
|
||||
for obj in &unreachable_refs {
|
||||
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj.as_ref()));
|
||||
let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
|
||||
let initial = initial_counts.get(&ptr).copied().unwrap_or(1);
|
||||
if obj.strong_count() > initial {
|
||||
resurrected_set.insert(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Transitive resurrection - objects reachable from resurrected are also resurrected
|
||||
// This is critical for cases like: Lazarus resurrects itself, its cargo should also survive
|
||||
let mut worklist: Vec<GcObjectPtr> = resurrected_set.iter().copied().collect();
|
||||
// Transitive resurrection
|
||||
let mut worklist: Vec<GcPtr> = resurrected_set.iter().copied().collect();
|
||||
while let Some(ptr) = worklist.pop() {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
|
||||
for child_ptr in referent_ptrs {
|
||||
let child_gc_ptr = GcObjectPtr(child_ptr);
|
||||
// If child is in unreachable set and not yet marked as resurrected
|
||||
let child_gc_ptr = GcPtr(child_ptr);
|
||||
if unreachable_set.contains(&child_gc_ptr) && resurrected_set.insert(child_gc_ptr) {
|
||||
worklist.push(child_gc_ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Partition into resurrected and truly dead
|
||||
// Partition into resurrected and truly dead
|
||||
let (resurrected, truly_dead): (Vec<_>, Vec<_>) =
|
||||
unreachable_refs.into_iter().partition(|obj| {
|
||||
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj.as_ref()));
|
||||
let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
|
||||
resurrected_set.contains(&ptr)
|
||||
});
|
||||
|
||||
let resurrected_count = resurrected.len();
|
||||
|
||||
if debug.contains(GcDebugFlags::STATS) {
|
||||
eprintln!(
|
||||
"gc: {} resurrected, {} truly dead",
|
||||
resurrected_count,
|
||||
resurrected.len(),
|
||||
truly_dead.len()
|
||||
);
|
||||
}
|
||||
|
||||
// 6e: Break cycles ONLY for truly dead objects (not resurrected)
|
||||
// Compute collected count: exclude instance dicts that are also in truly_dead.
|
||||
// In CPython 3.12+, instance dicts are managed inline and not separately tracked,
|
||||
// so they don't count toward the collected total.
|
||||
// Compute collected count (exclude instance dicts in truly_dead)
|
||||
let collected = {
|
||||
let dead_ptrs: HashSet<usize> = truly_dead
|
||||
.iter()
|
||||
@@ -672,7 +602,16 @@ impl GcState {
|
||||
truly_dead.len() - instance_dict_count
|
||||
};
|
||||
|
||||
// 6e-1: If DEBUG_SAVEALL is set, save truly dead objects to garbage
|
||||
// Promote survivors to next generation BEFORE tp_clear.
|
||||
// This matches CPython's order (move_legacy_finalizer_reachable → delete_garbage)
|
||||
// and ensures survivor_refs are dropped before tp_clear, so reachable objects
|
||||
// (e.g. LateFin) aren't kept alive beyond the deferred-drop phase.
|
||||
self.promote_survivors(generation, &survivor_refs);
|
||||
drop(survivor_refs);
|
||||
|
||||
// Resurrected objects stay tracked — just drop our references
|
||||
drop(resurrected);
|
||||
|
||||
if debug.contains(GcDebugFlags::SAVEALL) {
|
||||
let mut garbage_guard = self.garbage.lock();
|
||||
for obj_ref in truly_dead.iter() {
|
||||
@@ -681,12 +620,8 @@ impl GcState {
|
||||
}
|
||||
|
||||
if !truly_dead.is_empty() {
|
||||
// 6g: Break cycles by clearing references (tp_clear)
|
||||
// Weakrefs were already cleared in step 6c, but new weakrefs created
|
||||
// during __del__ (step 6d) can still be upgraded.
|
||||
//
|
||||
// Clear and destroy objects within a deferred drop context.
|
||||
// This prevents deadlocks from untrack calls during destruction.
|
||||
// Break cycles by clearing references (tp_clear)
|
||||
// Use deferred drop context to prevent stack overflow.
|
||||
rustpython_common::refcount::with_deferred_drops(|| {
|
||||
for obj_ref in truly_dead.iter() {
|
||||
if obj_ref.gc_has_clear() {
|
||||
@@ -694,19 +629,11 @@ impl GcState {
|
||||
drop(edges);
|
||||
}
|
||||
}
|
||||
// Drop truly_dead references, triggering actual deallocation
|
||||
drop(truly_dead);
|
||||
});
|
||||
}
|
||||
|
||||
// 6f: Resurrected objects stay in tracked_objects (they're still alive)
|
||||
// Just drop our references to them
|
||||
drop(resurrected);
|
||||
|
||||
// Promote survivors (reachable objects) to next generation
|
||||
self.promote_survivors(generation, &reachable);
|
||||
|
||||
// Reset gen0 count after collection (enables automatic GC to trigger again)
|
||||
// Reset gen0 count
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
|
||||
self.generations[generation].update_stats(collected, 0);
|
||||
@@ -714,39 +641,49 @@ impl GcState {
|
||||
(collected, 0)
|
||||
}
|
||||
|
||||
/// Promote surviving objects to the next generation
|
||||
fn promote_survivors(&self, from_gen: usize, survivors: &HashSet<GcObjectPtr>) {
|
||||
/// Promote surviving objects to the next generation.
|
||||
///
|
||||
/// `survivors` must be strong references (`PyObjectRef`) to keep objects alive,
|
||||
/// since the generation read locks are released before this is called.
|
||||
///
|
||||
/// Holds both source and destination list locks simultaneously to prevent
|
||||
/// a race where concurrent `untrack_object` reads a stale `gc_generation`
|
||||
/// and operates on the wrong list.
|
||||
fn promote_survivors(&self, from_gen: usize, survivors: &[PyObjectRef]) {
|
||||
if from_gen >= 2 {
|
||||
return; // Already in oldest generation
|
||||
}
|
||||
|
||||
let next_gen = from_gen + 1;
|
||||
|
||||
for &ptr in survivors {
|
||||
// Remove from current generation
|
||||
for gen_idx in 0..=from_gen {
|
||||
let mut gen_set = self.generation_objects[gen_idx].write();
|
||||
if gen_set.remove(&ptr) {
|
||||
// Decrement count for source generation
|
||||
let count = self.generations[gen_idx].count.load(Ordering::SeqCst);
|
||||
if count > 0 {
|
||||
self.generations[gen_idx]
|
||||
.count
|
||||
.fetch_sub(1, Ordering::SeqCst);
|
||||
}
|
||||
for obj_ref in survivors {
|
||||
let obj = obj_ref.as_ref();
|
||||
let ptr = NonNull::from(obj);
|
||||
let obj_gen = obj.gc_generation();
|
||||
if obj_gen as usize <= from_gen && obj_gen <= 2 {
|
||||
let src_gen = obj_gen as usize;
|
||||
|
||||
// Release before acquiring next lock
|
||||
drop(gen_set);
|
||||
// Lock both source and destination lists simultaneously.
|
||||
// Always ascending order (src_gen < next_gen) → no deadlock.
|
||||
let mut src = self.generation_lists[src_gen].write();
|
||||
let mut dst = self.generation_lists[next_gen].write();
|
||||
|
||||
// Add to next generation
|
||||
let mut next_set = self.generation_objects[next_gen].write();
|
||||
if next_set.insert(ptr) {
|
||||
// Increment count for target generation
|
||||
self.generations[next_gen]
|
||||
.count
|
||||
.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
break;
|
||||
// Re-check under locks: object might have been untracked concurrently
|
||||
if obj.gc_generation() != obj_gen || !obj.is_gc_tracked() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if unsafe { src.remove(ptr) }.is_some() {
|
||||
self.generations[src_gen]
|
||||
.count
|
||||
.fetch_sub(1, Ordering::SeqCst);
|
||||
|
||||
dst.push_front(ptr);
|
||||
self.generations[next_gen]
|
||||
.count
|
||||
.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
obj.set_gc_generation(next_gen as u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -757,47 +694,44 @@ impl GcState {
|
||||
self.permanent.count()
|
||||
}
|
||||
|
||||
/// Freeze all tracked objects (move to permanent generation)
|
||||
/// Freeze all tracked objects (move to permanent generation).
|
||||
/// Lock order: generation_lists[i] → permanent_list (consistent with unfreeze).
|
||||
pub fn freeze(&self) {
|
||||
// Move all objects from gen0-2 to permanent
|
||||
let mut objects_to_freeze: Vec<GcObjectPtr> = Vec::new();
|
||||
let mut count = 0usize;
|
||||
|
||||
for (gen_idx, generation) in self.generation_objects.iter().enumerate() {
|
||||
let mut gen_set = generation.write();
|
||||
objects_to_freeze.extend(gen_set.drain());
|
||||
for (gen_idx, gen_list) in self.generation_lists.iter().enumerate() {
|
||||
let mut list = gen_list.write();
|
||||
let mut perm = self.permanent_list.write();
|
||||
while let Some(ptr) = list.pop_front() {
|
||||
perm.push_front(ptr);
|
||||
unsafe { ptr.as_ref().set_gc_generation(GC_PERMANENT) };
|
||||
count += 1;
|
||||
}
|
||||
self.generations[gen_idx].count.store(0, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// Add to permanent set
|
||||
let mut permanent = self.permanent_objects.write();
|
||||
let count = objects_to_freeze.len();
|
||||
for ptr in objects_to_freeze {
|
||||
permanent.insert(ptr);
|
||||
}
|
||||
self.permanent.count.fetch_add(count, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Unfreeze all objects (move from permanent to gen2)
|
||||
/// Unfreeze all objects (move from permanent to gen2).
|
||||
/// Lock order: generation_lists[2] → permanent_list (consistent with freeze).
|
||||
pub fn unfreeze(&self) {
|
||||
let mut objects_to_unfreeze: Vec<GcObjectPtr> = Vec::new();
|
||||
let mut count = 0usize;
|
||||
|
||||
{
|
||||
let mut permanent = self.permanent_objects.write();
|
||||
objects_to_unfreeze.extend(permanent.drain());
|
||||
let mut gen2 = self.generation_lists[2].write();
|
||||
let mut perm_list = self.permanent_list.write();
|
||||
while let Some(ptr) = perm_list.pop_front() {
|
||||
gen2.push_front(ptr);
|
||||
unsafe { ptr.as_ref().set_gc_generation(2) };
|
||||
count += 1;
|
||||
}
|
||||
self.permanent.count.store(0, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// Add to generation 2
|
||||
let mut gen2 = self.generation_objects[2].write();
|
||||
let count = objects_to_unfreeze.len();
|
||||
for ptr in objects_to_unfreeze {
|
||||
gen2.insert(ptr);
|
||||
}
|
||||
self.generations[2].count.fetch_add(count, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Force-unlock all locks after fork() in the child process.
|
||||
///
|
||||
/// Reset all locks to unlocked state after fork().
|
||||
///
|
||||
/// After fork(), only the forking thread survives. Any lock held by another
|
||||
@@ -820,12 +754,10 @@ impl GcState {
|
||||
}
|
||||
self.permanent.reinit_stats_after_fork();
|
||||
|
||||
for rw in &self.generation_objects {
|
||||
for rw in &self.generation_lists {
|
||||
reinit_rwlock_after_fork(rw);
|
||||
}
|
||||
reinit_rwlock_after_fork(&self.permanent_objects);
|
||||
reinit_rwlock_after_fork(&self.tracked_objects);
|
||||
reinit_rwlock_after_fork(&self.finalized_objects);
|
||||
reinit_rwlock_after_fork(&self.permanent_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,21 +169,23 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
|
||||
let vtable = obj_ref.0.vtable;
|
||||
|
||||
// Untrack from GC BEFORE deallocation.
|
||||
// Must happen before memory is freed because intrusive list removal
|
||||
// reads the object's gc_pointers (prev/next).
|
||||
if obj_ref.is_gc_tracked() {
|
||||
let ptr = unsafe { NonNull::new_unchecked(obj) };
|
||||
if T::HAS_FREELIST {
|
||||
// Freelist types must untrack immediately to avoid race conditions:
|
||||
// a deferred untrack could remove a re-tracked entry after reuse.
|
||||
unsafe { crate::gc_state::gc_state().untrack_object(ptr) };
|
||||
} else {
|
||||
rustpython_common::refcount::try_defer_drop(move || {
|
||||
// untrack_object only removes the pointer address from a HashSet.
|
||||
// It does NOT dereference the pointer, so it's safe even after deallocation.
|
||||
unsafe {
|
||||
crate::gc_state::gc_state().untrack_object(ptr);
|
||||
}
|
||||
});
|
||||
unsafe {
|
||||
crate::gc_state::gc_state().untrack_object(ptr);
|
||||
}
|
||||
// Verify untrack cleared the tracked flag and generation
|
||||
debug_assert!(
|
||||
!obj_ref.is_gc_tracked(),
|
||||
"object still tracked after untrack_object"
|
||||
);
|
||||
debug_assert_eq!(
|
||||
obj_ref.gc_generation(),
|
||||
crate::object::GC_UNTRACKED,
|
||||
"gc_generation not reset after untrack_object"
|
||||
);
|
||||
}
|
||||
|
||||
// Extract child references before deallocation to break circular refs (tp_clear)
|
||||
@@ -257,6 +259,33 @@ bitflags::bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
/// GC generation constants
|
||||
pub(crate) const GC_UNTRACKED: u8 = 0xFF;
|
||||
pub(crate) const GC_PERMANENT: u8 = 3;
|
||||
|
||||
/// Link implementation for GC intrusive linked list tracking
|
||||
pub(crate) struct GcLink;
|
||||
|
||||
// SAFETY: PyObject (PyInner<Erased>) is heap-allocated and pinned in memory
|
||||
// once created. gc_pointers is at a fixed offset in PyInner.
|
||||
unsafe impl Link for GcLink {
|
||||
type Handle = NonNull<PyObject>;
|
||||
type Target = PyObject;
|
||||
|
||||
fn as_raw(handle: &NonNull<PyObject>) -> NonNull<PyObject> {
|
||||
*handle
|
||||
}
|
||||
|
||||
unsafe fn from_raw(ptr: NonNull<PyObject>) -> NonNull<PyObject> {
|
||||
ptr
|
||||
}
|
||||
|
||||
unsafe fn pointers(target: NonNull<PyObject>) -> NonNull<Pointers<PyObject>> {
|
||||
let inner_ptr = target.as_ptr() as *mut PyInner<Erased>;
|
||||
unsafe { NonNull::new_unchecked(&raw mut (*inner_ptr).gc_pointers) }
|
||||
}
|
||||
}
|
||||
|
||||
/// This is an actual python object. It consists of a `typ` which is the
|
||||
/// python class, and carries some rust payload optionally. This rust
|
||||
/// payload can be a rust float or rust int in case of float and int objects.
|
||||
@@ -266,6 +295,11 @@ pub(super) struct PyInner<T> {
|
||||
pub(super) vtable: &'static PyObjVTable,
|
||||
/// GC bits for free-threading (like ob_gc_bits)
|
||||
pub(super) gc_bits: PyAtomic<u8>,
|
||||
/// GC generation index (0-2=gen, GC_PERMANENT=permanent, GC_UNTRACKED=not tracked).
|
||||
/// Uses PyAtomic for interior mutability (writes happen through &self under list locks).
|
||||
pub(super) gc_generation: PyAtomic<u8>,
|
||||
/// Intrusive linked list pointers for GC generational tracking
|
||||
pub(super) gc_pointers: Pointers<PyObject>,
|
||||
|
||||
pub(super) typ: PyAtomicRef<PyType>, // __class__ member
|
||||
pub(super) dict: Option<InstanceDict>,
|
||||
@@ -810,6 +844,8 @@ impl<T: PyPayload + core::fmt::Debug> PyInner<T> {
|
||||
ref_count: RefCount::new(),
|
||||
vtable: PyObjVTable::of::<T>(),
|
||||
gc_bits: Radium::new(0),
|
||||
gc_generation: Radium::new(GC_UNTRACKED),
|
||||
gc_pointers: Pointers::new(),
|
||||
typ: PyAtomicRef::from(typ),
|
||||
dict: dict.map(InstanceDict::new),
|
||||
weak_list: WeakRefList::new(),
|
||||
@@ -1199,7 +1235,7 @@ impl PyObject {
|
||||
/// Mark the object as finalized. Should be called before __del__.
|
||||
/// _PyGC_SET_FINALIZED in Py_GIL_DISABLED mode.
|
||||
#[inline]
|
||||
fn set_gc_finalized(&self) {
|
||||
pub(crate) fn set_gc_finalized(&self) {
|
||||
self.set_gc_bit(GcBits::FINALIZED);
|
||||
}
|
||||
|
||||
@@ -1209,6 +1245,19 @@ impl PyObject {
|
||||
self.0.gc_bits.fetch_or(bit.bits(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Get the GC generation index for this object.
|
||||
#[inline]
|
||||
pub(crate) fn gc_generation(&self) -> u8 {
|
||||
self.0.gc_generation.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Set the GC generation index for this object.
|
||||
/// Must only be called while holding the generation list's write lock.
|
||||
#[inline]
|
||||
pub(crate) fn set_gc_generation(&self, generation: u8) {
|
||||
self.0.gc_generation.store(generation, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// _PyObject_GC_TRACK
|
||||
#[inline]
|
||||
pub(crate) fn set_gc_tracked(&self) {
|
||||
@@ -2028,6 +2077,8 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) {
|
||||
ref_count: RefCount::new(),
|
||||
vtable: PyObjVTable::of::<PyType>(),
|
||||
gc_bits: Radium::new(0),
|
||||
gc_generation: Radium::new(GC_UNTRACKED),
|
||||
gc_pointers: Pointers::new(),
|
||||
dict: None,
|
||||
weak_list: WeakRefList::new(),
|
||||
payload: type_payload,
|
||||
@@ -2040,6 +2091,8 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) {
|
||||
ref_count: RefCount::new(),
|
||||
vtable: PyObjVTable::of::<PyType>(),
|
||||
gc_bits: Radium::new(0),
|
||||
gc_generation: Radium::new(GC_UNTRACKED),
|
||||
gc_pointers: Pointers::new(),
|
||||
dict: None,
|
||||
weak_list: WeakRefList::new(),
|
||||
payload: object_payload,
|
||||
|
||||
@@ -8,4 +8,5 @@ pub use self::core::*;
|
||||
pub use self::ext::*;
|
||||
pub use self::payload::*;
|
||||
pub(crate) use core::SIZEOF_PYOBJECT_HEAD;
|
||||
pub(crate) use core::{GC_PERMANENT, GC_UNTRACKED, GcLink};
|
||||
pub use traverse::{MaybeTraverse, Traverse, TraverseFn};
|
||||
|
||||
Reference in New Issue
Block a user