@@ -188,6 +188,15 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
188188 ) ;
189189 }
190190
191+ // Capture freelist bucket hint before tp_clear empties the payload.
192+ // Size-based freelists (e.g. PyTuple) need the element count for bucket selection,
193+ // but clear() replaces elements with an empty slice.
194+ let freelist_hint = if T :: HAS_FREELIST {
195+ unsafe { T :: freelist_hint ( obj) }
196+ } else {
197+ 0
198+ } ;
199+
191200 // Extract child references before deallocation to break circular refs (tp_clear)
192201 let mut edges = Vec :: new ( ) ;
193202 if let Some ( clear_fn) = vtable. clear {
@@ -198,7 +207,7 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
198207 // Only exact types (not heaptype subclasses) go into the freelist,
199208 // because the pop site assumes the cached typ matches the base type.
200209 let pushed = if T :: HAS_FREELIST && obj_ref. class ( ) . heaptype_ext . is_none ( ) {
201- unsafe { T :: freelist_push ( obj) }
210+ unsafe { T :: freelist_push ( obj, freelist_hint ) }
202211 } else {
203212 false
204213 } ;
@@ -1008,6 +1017,11 @@ impl<T: PyPayload + core::fmt::Debug> PyInner<T> {
10081017 }
10091018}
10101019
1020+ /// Returns the allocation layout for `PyInner<T>`, for use in freelist Drop impls.
1021+ pub ( crate ) const fn pyinner_layout < T : PyPayload > ( ) -> core:: alloc:: Layout {
1022+ core:: alloc:: Layout :: new :: < PyInner < T > > ( )
1023+ }
1024+
10111025/// Thread-local freelist storage for reusing object allocations.
10121026///
10131027/// Wraps a `Vec<*mut PyObject>`. On thread teardown, `Drop` frees raw
@@ -2076,24 +2090,34 @@ impl<T: PyPayload + crate::object::MaybeTraverse + core::fmt::Debug> PyRef<T> {
20762090
20772091 // Try to reuse from freelist (exact type only, no dict, no heaptype)
20782092 let cached = if !has_dict && !is_heaptype {
2079- unsafe { T :: freelist_pop ( ) }
2093+ unsafe { T :: freelist_pop ( & payload ) }
20802094 } else {
20812095 None
20822096 } ;
20832097
20842098 let ptr = if let Some ( cached) = cached {
20852099 let inner = cached. as_ptr ( ) as * mut PyInner < T > ;
2086- unsafe {
2087- core:: ptr:: write ( & mut ( * inner) . ref_count , RefCount :: new ( ) ) ;
2088- ( * inner) . gc_bits . store ( 0 , Ordering :: Relaxed ) ;
2089- core:: ptr:: drop_in_place ( & mut ( * inner) . payload ) ;
2090- core:: ptr:: write ( & mut ( * inner) . payload , payload) ;
2091- // typ, vtable, slots are preserved; dict is None, weak_list was
2092- // cleared by drop_slow_inner before freelist push
2100+ // Verify the cached object's type matches the requested type.
2101+ // Subtypes sharing the same Rust payload (e.g. structseq -> PyTuple)
2102+ // may pop from the base type's freelist but need a different typ.
2103+ let cached_typ = unsafe { & * ( * inner) . typ } ;
2104+ if !core:: ptr:: eq ( cached_typ as * const _ , & * typ as * const _ ) {
2105+ // Type mismatch (e.g. structseq cached as PyTuple) - deallocate
2106+ unsafe { PyInner :: dealloc ( inner) } ;
2107+ let inner = PyInner :: new ( payload, typ, dict) ;
2108+ unsafe { NonNull :: new_unchecked ( inner. cast :: < Py < T > > ( ) ) }
2109+ } else {
2110+ unsafe {
2111+ core:: ptr:: write ( & mut ( * inner) . ref_count , RefCount :: new ( ) ) ;
2112+ ( * inner) . gc_bits . store ( 0 , Ordering :: Relaxed ) ;
2113+ core:: ptr:: drop_in_place ( & mut ( * inner) . payload ) ;
2114+ core:: ptr:: write ( & mut ( * inner) . payload , payload) ;
2115+ // typ and vtable are preserved from the cached object
2116+ }
2117+ // Drop the caller's typ since the cached object already holds one
2118+ drop ( typ) ;
2119+ unsafe { NonNull :: new_unchecked ( inner. cast :: < Py < T > > ( ) ) }
20932120 }
2094- // Drop the caller's typ since the cached object already holds one
2095- drop ( typ) ;
2096- unsafe { NonNull :: new_unchecked ( inner. cast :: < Py < T > > ( ) ) }
20972121 } else {
20982122 let inner = PyInner :: new ( payload, typ, dict) ;
20992123 unsafe { NonNull :: new_unchecked ( inner. cast :: < Py < T > > ( ) ) }
0 commit comments