Skip to content

Commit bf76333

Browse files
committed
dealloc the rigth way
1 parent 59739e6 commit bf76333

File tree

3 files changed

+59
-36
lines changed

3 files changed

+59
-36
lines changed

crates/vm/src/object/core.rs

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

84-
pub(super) unsafe fn drop_dealloc_obj<T: PyPayload>(x: *mut PyObject) {
85-
drop(unsafe { Box::from_raw(x as *mut PyInner<T>) });
84+
/// Default dealloc: handles __del__, weakref clearing, and memory free.
85+
/// Equivalent to subtype_dealloc in CPython.
86+
pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
87+
let obj_ref = unsafe { &*(obj as *const PyObject) };
88+
if let Err(()) = obj_ref.drop_slow_inner() {
89+
return; // resurrected by __del__
90+
}
91+
drop(unsafe { Box::from_raw(obj as *mut PyInner<T>) });
8692
}
8793
pub(super) unsafe fn debug_obj<T: PyPayload + core::fmt::Debug>(
8894
x: &PyObject,
@@ -1015,16 +1021,11 @@ impl PyObject {
10151021
Ok(())
10161022
}
10171023

1018-
/// Can only be called when ref_count has dropped to zero. `ptr` must be valid
1024+
/// _Py_Dealloc: dispatch to type's dealloc
10191025
#[inline(never)]
10201026
unsafe fn drop_slow(ptr: NonNull<Self>) {
1021-
if let Err(()) = unsafe { ptr.as_ref().drop_slow_inner() } {
1022-
// abort drop for whatever reason
1023-
return;
1024-
}
1025-
let drop_dealloc = unsafe { ptr.as_ref().0.vtable.drop_dealloc };
1026-
// call drop only when there are no references in scope - stacked borrows stuff
1027-
unsafe { drop_dealloc(ptr.as_ptr()) }
1027+
let dealloc = unsafe { ptr.as_ref().0.vtable.dealloc };
1028+
unsafe { dealloc(ptr.as_ptr()) }
10281029
}
10291030

10301031
/// # Safety

crates/vm/src/object/traverse_object.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use core::any::TypeId;
44
use crate::{
55
PyObject,
66
object::{
7-
Erased, InstanceDict, MaybeTraverse, PyInner, PyObjectPayload, debug_obj, drop_dealloc_obj,
7+
Erased, InstanceDict, MaybeTraverse, PyInner, PyObjectPayload, debug_obj, default_dealloc,
88
try_traverse_obj,
99
},
1010
};
@@ -13,7 +13,8 @@ use super::{Traverse, TraverseFn};
1313

1414
pub(in crate::object) struct PyObjVTable {
1515
pub(in crate::object) typeid: TypeId,
16-
pub(in crate::object) drop_dealloc: unsafe fn(*mut PyObject),
16+
/// dealloc: handles __del__, weakref clearing, and memory free.
17+
pub(in crate::object) dealloc: unsafe fn(*mut PyObject),
1718
pub(in crate::object) debug: unsafe fn(&PyObject, &mut fmt::Formatter<'_>) -> fmt::Result,
1819
pub(in crate::object) trace: Option<unsafe fn(&PyObject, &mut TraverseFn<'_>)>,
1920
}
@@ -22,7 +23,7 @@ impl PyObjVTable {
2223
pub const fn of<T: PyObjectPayload>() -> &'static Self {
2324
&Self {
2425
typeid: T::PAYLOAD_TYPE_ID,
25-
drop_dealloc: drop_dealloc_obj::<T>,
26+
dealloc: default_dealloc::<T>,
2627
debug: debug_obj::<T>,
2728
trace: const {
2829
if T::HAS_TRAVERSE {

crates/vm/src/vm/mod.rs

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::{
2020
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
2121
builtins::{
2222
self, PyBaseExceptionRef, PyDict, PyDictRef, PyInt, PyList, PyModule, PyStr, PyStrInterned,
23-
PyStrRef, PyTypeRef,
23+
PyStrRef, PyTypeRef, PyWeak,
2424
code::PyCode,
2525
dict::{PyDictItems, PyDictKeys, PyDictValues},
2626
pystr::AsPyStr,
@@ -628,18 +628,25 @@ impl VirtualMachine {
628628
self.finalize_modules_delete_special();
629629

630630
// Phase 2: Remove all modules from sys.modules (set values to None),
631-
// and collect references to module dicts preserving import order.
632-
// NOTE: CPython uses weakrefs here and relies on GC to collect cyclic garbage.
633-
// Since RustPython's GC is a stub, we store strong dict refs to ensure
634-
// phase 4 can clear them (breaking __globals__ cycles and triggering __del__).
635-
let module_dicts = self.finalize_remove_modules();
631+
// and collect weakrefs to modules preserving import order.
632+
// Also keeps strong refs (module_refs) to prevent premature deallocation.
633+
// CPython uses _PyGC_CollectNoFail() here to collect __globals__ cycles;
634+
// since RustPython has no working GC, we keep modules alive through
635+
// Phase 4 so their dicts can be explicitly cleared.
636+
let (module_weakrefs, module_refs) = self.finalize_remove_modules();
636637

637638
// Phase 3: Clear sys.modules dict
638639
self.finalize_clear_modules_dict();
639640

640641
// Phase 4: Clear module dicts in reverse import order using 2-pass algorithm.
641-
// This drops references to objects in module namespaces, triggering __del__.
642-
self.finalize_clear_module_dicts(&module_dicts);
642+
// All modules are still alive (held by module_refs), so all weakrefs are valid.
643+
// This breaks __globals__ cycles: dict entries set to None → functions freed →
644+
// __globals__ refs dropped → dict refcount decreases.
645+
self.finalize_clear_module_dicts(&module_weakrefs);
646+
647+
// Drop strong refs → modules freed with already-cleared dicts.
648+
// No __globals__ cycles remain (broken by Phase 4).
649+
drop(module_refs);
643650

644651
// Phase 5: Clear sys and builtins dicts last
645652
self.finalize_clear_sys_builtins_dict();
@@ -685,16 +692,17 @@ impl VirtualMachine {
685692
let _ = self.builtins.dict().set_item("_", none, self);
686693
}
687694

688-
/// Phase 2: Set all sys.modules values to None and collect module dicts.
689-
/// Returns a list of (name, dict) preserving import order.
690-
fn finalize_remove_modules(&self) -> Vec<(String, PyDictRef)> {
691-
let mut module_dicts = Vec::new();
695+
/// Phase 2: Set all sys.modules values to None and collect weakrefs to modules.
696+
/// Returns (weakrefs for Phase 4, strong refs to keep modules alive).
697+
fn finalize_remove_modules(&self) -> (Vec<(String, PyRef<PyWeak>)>, Vec<PyObjectRef>) {
698+
let mut module_weakrefs = Vec::new();
699+
let mut module_refs = Vec::new();
692700

693701
let Ok(modules) = self.sys_module.get_attr(identifier!(self, modules), self) else {
694-
return module_dicts;
702+
return (module_weakrefs, module_refs);
695703
};
696704
let Some(modules_dict) = modules.downcast_ref::<PyDict>() else {
697-
return module_dicts;
705+
return (module_weakrefs, module_refs);
698706
};
699707

700708
let none = self.ctx.none();
@@ -706,16 +714,19 @@ impl VirtualMachine {
706714
.map(|s| s.as_str().to_owned())
707715
.unwrap_or_default();
708716

709-
// Save reference to module dict for later clearing
710-
if let Some(module) = value.downcast_ref::<PyModule>() {
711-
module_dicts.push((name, module.dict()));
717+
// Save weakref and strong ref to module for later clearing
718+
if value.downcast_ref::<PyModule>().is_some() {
719+
if let Ok(weak) = value.downgrade(None, self) {
720+
module_weakrefs.push((name, weak));
721+
}
722+
module_refs.push(value.clone());
712723
}
713724

714725
// Set the value to None in sys.modules
715726
let _ = modules_dict.set_item(&*key, none.clone(), self);
716727
}
717728

718-
module_dicts
729+
(module_weakrefs, module_refs)
719730
}
720731

721732
/// Phase 3: Clear sys.modules dict.
@@ -728,27 +739,37 @@ impl VirtualMachine {
728739
}
729740

730741
/// Phase 4: Clear module dicts in reverse import order.
742+
/// All modules are alive (held by module_refs from Phase 2).
731743
/// Skips builtins and sys (they are cleared last in phase 5).
732-
fn finalize_clear_module_dicts(&self, module_dicts: &[(String, PyDictRef)]) {
744+
fn finalize_clear_module_dicts(&self, module_weakrefs: &[(String, PyRef<PyWeak>)]) {
733745
let sys_dict = self.sys_module.dict();
734746
let builtins_dict = self.builtins.dict();
735747

736748
// Iterate in reverse (last imported → first cleared)
737-
for (_name, dict) in module_dicts.iter().rev() {
749+
for (_name, weakref) in module_weakrefs.iter().rev() {
750+
// Try to upgrade weakref - skip if module was already freed
751+
let Some(module_obj) = weakref.upgrade() else {
752+
continue;
753+
};
754+
let Some(module) = module_obj.downcast_ref::<PyModule>() else {
755+
continue;
756+
};
757+
let module_dict = module.dict();
758+
738759
// Skip builtins and sys dicts (cleared last in phase 5)
739-
if dict.is(&sys_dict) || dict.is(&builtins_dict) {
760+
if module_dict.is(&sys_dict) || module_dict.is(&builtins_dict) {
740761
continue;
741762
}
742763

743764
// 2-pass clearing
744-
Self::module_clear_dict(dict, self);
765+
Self::module_clear_dict(&module_dict, self);
745766
}
746767
}
747768

748769
/// 2-pass module dict clearing (_PyModule_ClearDict algorithm).
749770
/// Pass 1: Set names starting with '_' (except __builtins__) to None.
750771
/// Pass 2: Set all remaining names (except __builtins__) to None.
751-
fn module_clear_dict(dict: &Py<PyDict>, vm: &VirtualMachine) {
772+
pub(crate) fn module_clear_dict(dict: &Py<PyDict>, vm: &VirtualMachine) {
752773
let none = vm.ctx.none();
753774

754775
// Pass 1: names starting with '_' (except __builtins__)

0 commit comments

Comments
 (0)