Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add FreeList<T> wrapper to drain cached objects on thread teardown
Replace Cell<Vec<*mut PyObject>> with Cell<FreeList<T>> which implements
Drop to properly deallocate PyInner<T> when threads exit.
Also fix freelist_pop safety doc to match actual contract.
  • Loading branch information
youknowone committed Mar 4, 2026
commit c0b33a68842c858ae95ccb7d81e71df39ea6c396
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ impl fmt::Debug for PyDict {
}

thread_local! {
static DICT_FREELIST: Cell<Vec<*mut PyObject>> = const { Cell::new(Vec::new()) };
static DICT_FREELIST: Cell<crate::object::FreeList<PyDict>> = const { Cell::new(crate::object::FreeList::new()) };
}

impl PyPayload for PyDict {
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ impl PyFloat {
}

thread_local! {
static FLOAT_FREELIST: Cell<Vec<*mut PyObject>> = const { Cell::new(Vec::new()) };
static FLOAT_FREELIST: Cell<crate::object::FreeList<PyFloat>> = const { Cell::new(crate::object::FreeList::new()) };
}

impl PyPayload for PyFloat {
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ unsafe impl Traverse for PyList {
}

thread_local! {
static LIST_FREELIST: Cell<Vec<*mut PyObject>> = const { Cell::new(Vec::new()) };
static LIST_FREELIST: Cell<crate::object::FreeList<PyList>> = const { Cell::new(crate::object::FreeList::new()) };
}

impl PyPayload for PyList {
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct PySlice {
}

thread_local! {
static SLICE_FREELIST: Cell<Vec<*mut PyObject>> = const { Cell::new(Vec::new()) };
static SLICE_FREELIST: Cell<crate::object::FreeList<PySlice>> = const { Cell::new(crate::object::FreeList::new()) };
}

impl PyPayload for PySlice {
Expand Down
49 changes: 43 additions & 6 deletions crates/vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -822,13 +822,50 @@ impl<T: PyPayload + core::fmt::Debug> PyInner<T> {
}
}

/// Drop a freelist-cached object, properly deallocating the `PyInner<T>`.
/// Thread-local freelist storage that properly deallocates cached objects
/// on thread teardown.
///
/// # Safety
/// `ptr` must point to a valid `PyInner<T>` allocation that was stored in a freelist.
#[allow(dead_code)]
pub(crate) unsafe fn drop_freelist_object<T: PyPayload>(ptr: *mut PyObject) {
drop(unsafe { Box::from_raw(ptr as *mut PyInner<T>) });
/// Wraps a `Vec<*mut PyObject>` and implements `Drop` to convert each
/// raw pointer back into `Box<PyInner<T>>` for proper deallocation.
pub(crate) struct FreeList<T: PyPayload> {
items: Vec<*mut PyObject>,
_marker: core::marker::PhantomData<T>,
}

impl<T: PyPayload> FreeList<T> {
pub(crate) const fn new() -> Self {
Self {
items: Vec::new(),
_marker: core::marker::PhantomData,
}
}
}

impl<T: PyPayload> Default for FreeList<T> {
fn default() -> Self {
Self::new()
}
}

impl<T: PyPayload> Drop for FreeList<T> {
fn drop(&mut self) {
for ptr in self.items.drain(..) {
drop(unsafe { Box::from_raw(ptr as *mut PyInner<T>) });
}
}
}

impl<T: PyPayload> core::ops::Deref for FreeList<T> {
type Target = Vec<*mut PyObject>;
fn deref(&self) -> &Self::Target {
&self.items
}
}

impl<T: PyPayload> core::ops::DerefMut for FreeList<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.items
}
}

/// The `PyObjectRef` is one of the most used types. It is a reference to a
Expand Down
4 changes: 3 additions & 1 deletion crates/vm/src/object/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static {
/// reinitialize `ref_count`, `gc_bits`, and `payload`.
///
/// # Safety
/// The returned pointer (if Some) points to uninitialized/stale payload.
/// The returned pointer (if Some) must point to a valid `PyInner<Self>`
/// whose payload is still initialized from a previous allocation. The caller
/// will drop and overwrite `payload` before reuse.
#[inline]
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
None
Expand Down
Loading