Object header slimming: prefix allocation for ObjExt (#7334)

* Object header slimming: prefix allocation for ObjExt

Extract dict, weak_list, and slots fields from PyInner<T> into a
separate ObjExt struct allocated as a prefix before PyInner using
Layout::extend(). Objects that don't need these fields (int, str,
float, list, tuple, dict, etc.) skip the prefix entirely.

- Add HAS_WEAKREF flag to PyTypeFlags for per-type weakref control
- Add HAS_EXT bit to GcBits indicating prefix presence
- Define ObjExt struct with dict, weak_list, slots
- Shrink PyInner header from ~80-88 bytes to ~32 bytes for lightweight objects
- Update all accessor methods to go through ext_ref()
- Update bootstrap type hierarchy to use prefix allocation
- Add __weakref__ getset descriptor for heap types
- Set HAS_WEAKREF on builtin types that support weak references
- Remove test_weak_keyed_bad_delitem expectedFailure (now passes)

* Add HAS_WEAKREF to _asyncio Future/Task, rename weakref helpers

- Add HAS_WEAKREF flag to PyFuture and PyTask (matches CPython)
- Rename subtype_getweakref/setweakref to subtype_get_weakref/set_weakref
  to fix cspell unknown word lint

* Add HAS_WEAKREF to array, deque, _grouper; remove expectedFailure markers

- Add HAS_WEAKREF to PyArray and PyDeque (matches CPython)
- Add HAS_WEAKREF to PyItertoolsGrouper (internal use by groupby)
- Remove 6 expectedFailure markers from test_dataclasses for weakref/slots
  tests that now pass

* Add HAS_WEAKREF to code, union, partial, lock, IO, mmap, sre, sqlite3, typevar types

Add HAS_WEAKREF flag to built-in types that support weakref:
- PyCode, PyUnion, PyPartial, Lock, RLock
- All IO base/concrete classes (_IOBase, _RawIOBase, _BufferedIOBase,
  _TextIOBase, BufferedReader, BufferedWriter, BufferedRandom,
  BufferedRWPair, TextIOWrapper, StringIO, BytesIO, FileIO,
  WindowsConsoleIO)
- PyMmap, sre Pattern, sqlite3 Connection/Cursor
- TypeVar, ParamSpec, ParamSpecArgs, ParamSpecKwargs, TypeVarTuple

Remove 3 expectedFailure markers from test_descr for now-passing tests.

* Add HAS_DICT to type flags and handle non-METHOD/CLASS in descr_get

- Add HAS_DICT flag to PyType (type metaclass) alongside existing
  HAS_WEAKREF. All type objects are instances of type and need dict
  support, matching CPython's PyType_Type.
- Replace unimplemented!() in PyMethodDescriptor::descr_get with
  fallback to bind obj directly, matching CPython's method_get which
  uses PyCFunction_NewEx for non-METH_METHOD methods.

* Fix ext detection, HeapMethodDef ownership, WASM error

- Remove HAS_EXT gc_bits flag; detect ext from type flags
  using raw pointer reads to avoid Stacked Borrows violations
- Store HeapMethodDef owner in payload instead of dict hack
- Clear dict entries in gc_clear_raw to break cycles
- Add WASM error fallback when exception serialization fails
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-03-06 01:19:22 +09:00
1 parent 3b91466f62
commit 04cf5dacca
40 files changed
+565 -257

No files matched your search

+2 -2
View File
@@ -6,6 +6,7 @@ bindgen
bitand
bitflags
bitor
bitvec
bitxor
bstr
byteorder
@@ -58,6 +59,7 @@ powi
prepended
punct
replacen
retag
rmatch
rposition
rsplitn
@@ -89,5 +91,3 @@ widestring
winapi
winresource
winsock
bitvec
Bitvec
-6
View File
@@ -3672,7 +3672,6 @@ class TestSlots(unittest.TestCase):
self.assertEqual(obj.a, 'a')
self.assertEqual(obj.b, 'b')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_slots_no_weakref(self):
@dataclass(slots=True)
class A:
@@ -3687,7 +3686,6 @@ class TestSlots(unittest.TestCase):
with self.assertRaises(AttributeError):
a.__weakref__
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_slots_weakref(self):
@dataclass(slots=True, weakref_slot=True)
class A:
@@ -3748,7 +3746,6 @@ class TestSlots(unittest.TestCase):
"weakref_slot is True but slots is False"):
B = make_dataclass('B', [('a', int),], weakref_slot=True)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_weakref_slot_subclass_weakref_slot(self):
@dataclass(slots=True, weakref_slot=True)
class Base:
@@ -3767,7 +3764,6 @@ class TestSlots(unittest.TestCase):
a_ref = weakref.ref(a)
self.assertIs(a.__weakref__, a_ref)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_weakref_slot_subclass_no_weakref_slot(self):
@dataclass(slots=True, weakref_slot=True)
class Base:
@@ -3785,7 +3781,6 @@ class TestSlots(unittest.TestCase):
a_ref = weakref.ref(a)
self.assertIs(a.__weakref__, a_ref)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_weakref_slot_normal_base_weakref_slot(self):
class Base:
__slots__ = ('__weakref__',)
@@ -3830,7 +3825,6 @@ class TestSlots(unittest.TestCase):
self.assertTrue(B.__weakref__)
B()
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_dataclass_derived_generic_from_base(self):
T = typing.TypeVar('T')
-3
View File
@@ -1321,7 +1321,6 @@ class ClassPropertiesAndMethods(unittest.TestCase):
with self.assertRaisesRegex(AttributeError, "'X' object has no attribute 'a'"):
X().a
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_slots_special(self):
# Testing __dict__ and __weakref__ in __slots__...
class D(object):
@@ -2294,7 +2293,6 @@ class ClassPropertiesAndMethods(unittest.TestCase):
self.assertIn(i, p10)
self.assertNotIn(10, p10)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_weakrefs(self):
# Testing weak references...
import weakref
@@ -3976,7 +3974,6 @@ class ClassPropertiesAndMethods(unittest.TestCase):
o = trash(o)
del o
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_slots_multiple_inheritance(self):
# SF bug 575229, multiple inheritance w/ slots dumps core
class A(object):
-1
View File
@@ -1862,7 +1862,6 @@ class MappingTestCase(TestBase):
self.assertEqual(len(d), 1)
self.assertEqual(list(d.items()), [('something else', o2)])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_weak_keyed_bad_delitem(self):
d = weakref.WeakKeyDictionary()
o = Object('1')
+2 -2
View File
@@ -160,7 +160,7 @@ pub(crate) mod _asyncio {
}
#[pyclass(
flags(BASETYPE, HAS_DICT),
flags(BASETYPE, HAS_DICT, HAS_WEAKREF),
with(Constructor, Initializer, Destructor, Representable, Iterable)
)]
impl PyFuture {
@@ -1169,7 +1169,7 @@ pub(crate) mod _asyncio {
}
#[pyclass(
flags(BASETYPE, HAS_DICT),
flags(BASETYPE, HAS_DICT, HAS_WEAKREF),
with(Constructor, Initializer, Destructor, Representable, Iterable)
)]
impl PyTask {
+5 -2
View File
@@ -976,7 +976,7 @@ mod _sqlite3 {
}
}
#[pyclass(with(Constructor, Callable, Initializer), flags(BASETYPE))]
#[pyclass(with(Constructor, Callable, Initializer), flags(BASETYPE, HAS_WEAKREF))]
impl Connection {
fn drop_db(&self) {
self.db.lock().take();
@@ -1629,7 +1629,10 @@ mod _sqlite3 {
size: Option<c_int>,
}
#[pyclass(with(Constructor, Initializer, IterNext, Iterable), flags(BASETYPE))]
#[pyclass(
with(Constructor, Initializer, IterNext, Iterable),
flags(BASETYPE, HAS_WEAKREF)
)]
impl Cursor {
fn new(
connection: PyRef<Connection>,
+1 -1
View File
@@ -698,7 +698,7 @@ mod array {
}
#[pyclass(
flags(BASETYPE),
flags(BASETYPE, HAS_WEAKREF),
with(
Comparable,
AsBuffer,
+1 -1
View File
@@ -851,7 +851,7 @@ mod mmap {
#[pyclass(
with(Constructor, AsMapping, AsSequence, AsBuffer, Representable),
flags(BASETYPE)
flags(BASETYPE, HAS_WEAKREF)
)]
impl PyMmap {
fn as_bytes_mut(&self) -> BorrowedValueMut<'_, [u8]> {
+1 -1
View File
@@ -317,7 +317,7 @@ mod re {
#[pyfunction]
fn purge(_vm: &VirtualMachine) {}
#[pyclass]
#[pyclass(flags(HAS_WEAKREF))]
impl PyPattern {
#[pymethod(name = "match")]
fn match_(&self, text: PyStrRef) -> Option<PyMatch> {
+4 -1
View File
@@ -40,7 +40,10 @@ impl PyPayload for PyAsyncGen {
}
}
#[pyclass(flags(DISALLOW_INSTANTIATION), with(PyRef, Representable, Destructor))]
#[pyclass(
flags(DISALLOW_INSTANTIATION, HAS_WEAKREF),
with(PyRef, Representable, Destructor)
)]
impl PyAsyncGen {
pub const fn as_coro(&self) -> &Coro {
&self.inner
+4 -2
View File
@@ -16,6 +16,8 @@ pub struct PyNativeFunction {
pub(crate) value: &'static PyMethodDef,
pub(crate) zelf: Option<PyObjectRef>,
pub(crate) module: Option<&'static PyStrInterned>, // None for bound method
/// Prevent HeapMethodDef from being freed while this function references it
pub(crate) _method_def_owner: Option<PyObjectRef>,
}
impl PyPayload for PyNativeFunction {
@@ -126,7 +128,7 @@ impl Representable for PyNativeFunction {
#[pyclass(
with(Callable, Comparable, Representable),
flags(HAS_DICT, DISALLOW_INSTANTIATION)
flags(HAS_DICT, HAS_WEAKREF, DISALLOW_INSTANTIATION)
)]
impl PyNativeFunction {
#[pygetset]
@@ -210,7 +212,7 @@ pub struct PyNativeMethod {
// All Python-visible behavior (getters, slots) is registered by PyNativeFunction::extend_class.
// PyNativeMethod only extends the Rust-side struct with the defining class reference.
// The func field at offset 0 (#[repr(C)]) allows NativeFunctionOrMethod to read it safely.
#[pyclass(flags(HAS_DICT, DISALLOW_INSTANTIATION))]
#[pyclass(flags(HAS_DICT, HAS_WEAKREF, DISALLOW_INSTANTIATION))]
impl PyNativeMethod {}
impl fmt::Debug for PyNativeMethod {
+1 -1
View File
@@ -125,7 +125,7 @@ impl PyClassMethod {
#[pyclass(
with(GetDescriptor, Constructor, Initializer, Representable),
flags(BASETYPE, HAS_DICT)
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl PyClassMethod {
#[pygetset]
+1 -1
View File
@@ -597,7 +597,7 @@ impl Constructor for PyCode {
}
}
#[pyclass(with(Representable, Constructor))]
#[pyclass(with(Representable, Constructor), flags(HAS_WEAKREF))]
impl PyCode {
#[pygetset]
const fn co_posonlyargcount(&self) -> usize {
+1 -1
View File
@@ -32,7 +32,7 @@ impl PyPayload for PyCoroutine {
}
#[pyclass(
flags(DISALLOW_INSTANTIATION),
flags(DISALLOW_INSTANTIATION, HAS_WEAKREF),
with(Py, IterNext, Representable, Destructor)
)]
impl PyCoroutine {
+4 -2
View File
@@ -37,6 +37,8 @@ pub struct PyMethodDescriptor {
pub method: &'static PyMethodDef,
// vectorcall: vector_call_func,
pub objclass: &'static Py<PyType>, // TODO: move to tp_members
/// Prevent HeapMethodDef from being freed while this descriptor references it
pub(crate) _method_def_owner: Option<PyObjectRef>,
}
impl PyMethodDescriptor {
@@ -49,6 +51,7 @@ impl PyMethodDescriptor {
},
method,
objclass: typ,
_method_def_owner: None,
}
}
}
@@ -88,13 +91,12 @@ impl GetDescriptor for PyMethodDescriptor {
} else if descr.method.flags.contains(PyMethodFlags::CLASS) {
obj.class().to_owned().into()
} else {
unimplemented!()
obj
}
}
None if descr.method.flags.contains(PyMethodFlags::CLASS) => cls.unwrap(),
None => return Ok(zelf),
};
// Ok(descr.method.build_bound_method(&vm.ctx, bound, class).into())
Ok(descr.bind(bound, &vm.ctx).into())
}
}
+2 -2
View File
@@ -717,7 +717,7 @@ impl PyPayload for PyFunction {
#[pyclass(
with(GetDescriptor, Callable, Representable, Constructor),
flags(HAS_DICT, METHOD_DESCRIPTOR)
flags(HAS_DICT, HAS_WEAKREF, METHOD_DESCRIPTOR)
)]
impl PyFunction {
#[pygetset]
@@ -1170,7 +1170,7 @@ impl PyBoundMethod {
#[pyclass(
with(Callable, Comparable, GetAttr, Constructor, Representable),
flags(IMMUTABLETYPE)
flags(IMMUTABLETYPE, HAS_WEAKREF)
)]
impl PyBoundMethod {
#[pymethod]
+1 -1
View File
@@ -34,7 +34,7 @@ impl PyPayload for PyGenerator {
}
#[pyclass(
flags(DISALLOW_INSTANTIATION),
flags(DISALLOW_INSTANTIATION, HAS_WEAKREF),
with(Py, IterNext, Iterable, Representable, Destructor)
)]
impl PyGenerator {
+1 -1
View File
@@ -84,7 +84,7 @@ impl Constructor for PyGenericAlias {
Iterable,
Representable
),
flags(BASETYPE)
flags(BASETYPE, HAS_WEAKREF)
)]
impl PyGenericAlias {
pub fn new(
+1 -1
View File
@@ -549,7 +549,7 @@ impl Py<PyMemoryView> {
Iterable,
Representable
),
flags(SEQUENCE)
flags(SEQUENCE, HAS_WEAKREF)
)]
impl PyMemoryView {
#[pyclassmethod]
+4 -1
View File
@@ -286,7 +286,10 @@ impl Py<PyModule> {
}
}
#[pyclass(with(GetAttr, Initializer, Representable), flags(BASETYPE, HAS_DICT))]
#[pyclass(
with(GetAttr, Initializer, Representable),
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl PyModule {
#[pyslot]
fn slot_new(cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
+1 -1
View File
@@ -28,7 +28,7 @@ impl PyPayload for PyNamespace {
impl DefaultConstructor for PyNamespace {}
#[pyclass(
flags(BASETYPE, HAS_DICT),
flags(BASETYPE, HAS_DICT, HAS_WEAKREF),
with(Constructor, Initializer, Comparable, Representable)
)]
impl PyNamespace {
+4
View File
@@ -464,6 +464,8 @@ impl PyBaseObject {
if both_mutable || both_module {
let has_dict =
|typ: &Py<PyType>| typ.slots.flags.has_feature(PyTypeFlags::HAS_DICT);
let has_weakref =
|typ: &Py<PyType>| typ.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF);
// Compare slots tuples
let slots_equal = match (
current_cls
@@ -484,6 +486,8 @@ impl PyBaseObject {
if current_cls.slots.basicsize != cls.slots.basicsize
|| !slots_equal
|| has_dict(current_cls) != has_dict(&cls)
|| has_weakref(current_cls) != has_weakref(&cls)
|| current_cls.slots.member_count != cls.slots.member_count
{
return Err(vm.new_type_error(format!(
"__class__ assignment: '{}' object layout differs from '{}'",
+4 -1
View File
@@ -65,7 +65,10 @@ impl GetDescriptor for PyProperty {
}
}
#[pyclass(with(Constructor, Initializer, GetDescriptor), flags(BASETYPE))]
#[pyclass(
with(Constructor, Initializer, GetDescriptor),
flags(BASETYPE, HAS_WEAKREF)
)]
impl PyProperty {
// Helper method to get property name
// Returns the name if available, None if not found, or propagates errors
+2 -2
View File
@@ -531,7 +531,7 @@ fn reduce_set(
AsNumber,
Representable
),
flags(BASETYPE, _MATCH_SELF)
flags(BASETYPE, _MATCH_SELF, HAS_WEAKREF)
)]
impl PySet {
fn __len__(&self) -> usize {
@@ -996,7 +996,7 @@ impl Constructor for PyFrozenSet {
}
#[pyclass(
flags(BASETYPE, _MATCH_SELF),
flags(BASETYPE, _MATCH_SELF, HAS_WEAKREF),
with(
Constructor,
AsSequence,
+1 -1
View File
@@ -88,7 +88,7 @@ impl Initializer for PyStaticMethod {
#[pyclass(
with(Callable, GetDescriptor, Constructor, Initializer, Representable),
flags(BASETYPE, HAS_DICT)
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl PyStaticMethod {
#[pygetset]
+155 -114
View File
@@ -562,6 +562,14 @@ impl PyType {
slots.flags |= PyTypeFlags::HAS_DICT
}
// Inherit HAS_WEAKREF from any base in MRO that has it
if mro
.iter()
.any(|b| b.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF))
{
slots.flags |= PyTypeFlags::HAS_WEAKREF
}
// Inherit SEQUENCE and MAPPING flags from base classes
Self::inherit_patma_flags(&mut slots, &bases);
@@ -623,6 +631,9 @@ impl PyType {
if base.slots.flags.has_feature(PyTypeFlags::HAS_DICT) {
slots.flags |= PyTypeFlags::HAS_DICT
}
if base.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF) {
slots.flags |= PyTypeFlags::HAS_WEAKREF
}
// Inherit SEQUENCE and MAPPING flags from base class
// For static types, we only have a single base
@@ -1083,7 +1094,7 @@ impl Py<PyType> {
AsNumber,
Representable
),
flags(BASETYPE)
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl PyType {
#[pygetset]
@@ -1709,141 +1720,125 @@ impl Constructor for PyType {
attributes.insert(identifier!(vm, __hash__), vm.ctx.none.clone().into());
}
let (heaptype_slots, add_dict): (Option<PyRef<PyTuple<PyStrRef>>>, bool) =
if let Some(x) = attributes.get(identifier!(vm, __slots__)) {
// Check if __slots__ is bytes - not allowed
if x.class().is(vm.ctx.types.bytes_type) {
return Err(vm.new_type_error("__slots__ items must be strings, not 'bytes'"));
}
let (heaptype_slots, add_dict, add_weakref): (
Option<PyRef<PyTuple<PyStrRef>>>,
bool,
bool,
) = if let Some(x) = attributes.get(identifier!(vm, __slots__)) {
// Check if __slots__ is bytes - not allowed
if x.class().is(vm.ctx.types.bytes_type) {
return Err(vm.new_type_error("__slots__ items must be strings, not 'bytes'"));
}
let slots = if x.class().is(vm.ctx.types.str_type) {
let x = unsafe { x.downcast_unchecked_ref::<PyStr>() };
PyTuple::new_ref_typed(vec![x.to_owned()], &vm.ctx)
} else {
let iter = x.get_iter(vm)?;
let elements = {
let mut elements = Vec::new();
while let PyIterReturn::Return(element) = iter.next(vm)? {
// Check if any slot item is bytes
if element.class().is(vm.ctx.types.bytes_type) {
return Err(vm.new_type_error(
"__slots__ items must be strings, not 'bytes'",
));
}
elements.push(element);
}
elements
};
let tuple = elements.into_pytuple(vm);
tuple.try_into_typed(vm)?
};
// Check if base has itemsize > 0 - can't add arbitrary slots to variable-size types
// Types like int, bytes, tuple have itemsize > 0 and don't allow custom slots
// But types like weakref.ref have itemsize = 0 and DO allow slots
let has_custom_slots = slots
.iter()
.any(|s| !matches!(s.as_bytes(), b"__dict__" | b"__weakref__"));
if has_custom_slots && base.slots.itemsize > 0 {
return Err(vm.new_type_error(format!(
"nonempty __slots__ not supported for subtype of '{}'",
base.name()
)));
}
// Validate slot names and track duplicates
let mut seen_dict = false;
let mut seen_weakref = false;
for slot in slots.iter() {
// Use isidentifier for validation (handles Unicode properly)
if !slot.isidentifier() {
return Err(vm.new_type_error("__slots__ must be identifiers"));
}
let slot_name = slot.as_bytes();
// Check for duplicate __dict__
if slot_name == b"__dict__" {
if seen_dict {
let slots = if x.class().is(vm.ctx.types.str_type) {
let x = unsafe { x.downcast_unchecked_ref::<PyStr>() };
PyTuple::new_ref_typed(vec![x.to_owned()], &vm.ctx)
} else {
let iter = x.get_iter(vm)?;
let elements = {
let mut elements = Vec::new();
while let PyIterReturn::Return(element) = iter.next(vm)? {
// Check if any slot item is bytes
if element.class().is(vm.ctx.types.bytes_type) {
return Err(
vm.new_type_error("__dict__ slot disallowed: we already got one")
vm.new_type_error("__slots__ items must be strings, not 'bytes'")
);
}
seen_dict = true;
elements.push(element);
}
elements
};
let tuple = elements.into_pytuple(vm);
tuple.try_into_typed(vm)?
};
// Check for duplicate __weakref__
if slot_name == b"__weakref__" {
if seen_weakref {
return Err(vm.new_type_error(
"__weakref__ slot disallowed: we already got one",
));
}
seen_weakref = true;
}
// Check if base has itemsize > 0 - can't add arbitrary slots to variable-size types
// Types like int, bytes, tuple have itemsize > 0 and don't allow custom slots
// But types like weakref.ref have itemsize = 0 and DO allow slots
let has_custom_slots = slots
.iter()
.any(|s| !matches!(s.as_bytes(), b"__dict__" | b"__weakref__"));
if has_custom_slots && base.slots.itemsize > 0 {
return Err(vm.new_type_error(format!(
"nonempty __slots__ not supported for subtype of '{}'",
base.name()
)));
}
// Check if slot name conflicts with class attributes
if attributes.contains_key(vm.ctx.intern_str(slot.as_wtf8())) {
return Err(vm.new_value_error(format!(
"'{}' in __slots__ conflicts with a class variable",
slot.as_wtf8()
)));
}
// Validate slot names and track duplicates
let mut seen_dict = false;
let mut seen_weakref = false;
for slot in slots.iter() {
// Use isidentifier for validation (handles Unicode properly)
if !slot.isidentifier() {
return Err(vm.new_type_error("__slots__ must be identifiers"));
}
// Check if base class already has __dict__ - can't redefine it
if seen_dict && base.slots.flags.has_feature(PyTypeFlags::HAS_DICT) {
return Err(vm.new_type_error("__dict__ slot disallowed: we already got one"));
let slot_name = slot.as_bytes();
// Check for duplicate __dict__
if slot_name == b"__dict__" {
if seen_dict {
return Err(
vm.new_type_error("__dict__ slot disallowed: we already got one")
);
}
seen_dict = true;
}
// Check if base class already has __weakref__ - can't redefine it
// A base has weakref support if:
// 1. It's a heap type without explicit __slots__ (automatic weakref), OR
// 2. It's a heap type with __weakref__ in its __slots__
if seen_weakref {
let base_has_weakref = if let Some(ref ext) = base.heaptype_ext {
match &ext.slots {
// Heap type without __slots__ - has automatic weakref
None => true,
// Heap type with __slots__ - check if __weakref__ is in slots
Some(base_slots) => {
base_slots.iter().any(|s| s.as_bytes() == b"__weakref__")
}
}
} else {
// Builtin type - check if it has __weakref__ descriptor
let weakref_name = vm.ctx.intern_str("__weakref__");
base.attributes.read().contains_key(weakref_name)
};
if base_has_weakref {
// Check for duplicate __weakref__
if slot_name == b"__weakref__" {
if seen_weakref {
return Err(
vm.new_type_error("__weakref__ slot disallowed: we already got one")
);
}
seen_weakref = true;
}
// Check if __dict__ is in slots
let dict_name = "__dict__";
let has_dict = slots.iter().any(|s| s.as_wtf8() == dict_name);
// Check if slot name conflicts with class attributes
if attributes.contains_key(vm.ctx.intern_str(slot.as_wtf8())) {
return Err(vm.new_value_error(format!(
"'{}' in __slots__ conflicts with a class variable",
slot.as_wtf8()
)));
}
}
// Filter out __dict__ from slots
let filtered_slots = if has_dict {
let filtered: Vec<PyStrRef> = slots
.iter()
.filter(|s| s.as_wtf8() != dict_name)
.cloned()
.collect();
PyTuple::new_ref_typed(filtered, &vm.ctx)
} else {
slots
};
// Check if base class already has __dict__ - can't redefine it
if seen_dict && base.slots.flags.has_feature(PyTypeFlags::HAS_DICT) {
return Err(vm.new_type_error("__dict__ slot disallowed: we already got one"));
}
(Some(filtered_slots), has_dict)
// Check if base class already has __weakref__ - can't redefine it
if seen_weakref && base.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF) {
return Err(vm.new_type_error("__weakref__ slot disallowed: we already got one"));
}
// Check if __dict__ or __weakref__ is in slots
let dict_name = "__dict__";
let weakref_name = "__weakref__";
let has_dict = slots.iter().any(|s| s.as_wtf8() == dict_name);
let add_weakref = seen_weakref;
// Filter out __dict__ and __weakref__ from slots
// (they become descriptors, not member slots)
let filtered_slots = if has_dict || add_weakref {
let filtered: Vec<PyStrRef> = slots
.iter()
.filter(|s| s.as_wtf8() != dict_name && s.as_wtf8() != weakref_name)
.cloned()
.collect();
PyTuple::new_ref_typed(filtered, &vm.ctx)
} else {
(None, false)
slots
};
(Some(filtered_slots), has_dict, add_weakref)
} else {
(None, false, false)
};
// FIXME: this is a temporary fix. multi bases with multiple slots will break object
let base_member_count = bases
.iter()
@@ -1867,6 +1862,14 @@ impl Constructor for PyType {
flags |= PyTypeFlags::HAS_DICT | PyTypeFlags::MANAGED_DICT;
}
// Add HAS_WEAKREF if:
// 1. __slots__ is not defined (automatic weakref support), OR
// 2. __weakref__ is in __slots__
let may_add_weakref = !base.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF);
if (heaptype_slots.is_none() && may_add_weakref) || add_weakref {
flags |= PyTypeFlags::HAS_WEAKREF;
}
let (slots, heaptype_ext) = {
let slots = PyTypeSlots {
flags,
@@ -1965,6 +1968,29 @@ impl Constructor for PyType {
}
}
// Add __weakref__ descriptor for types with HAS_WEAKREF
if typ.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF) {
let __weakref__ = vm.ctx.intern_str("__weakref__");
let has_inherited_weakref = typ
.mro
.read()
.iter()
.any(|base| base.attributes.read().contains_key(&__weakref__));
if !typ.attributes.read().contains_key(&__weakref__) && !has_inherited_weakref {
unsafe {
let descriptor = vm.ctx.new_getset(
"__weakref__",
&typ,
subtype_get_weakref,
subtype_set_weakref,
);
typ.attributes
.write()
.insert(__weakref__, descriptor.into());
}
}
}
// Set __doc__ to None if not already present in the type's dict
// This matches CPython's behavior in type_dict_set_doc (typeobject.c)
// which ensures every type has a __doc__ entry in its dict
@@ -2400,6 +2426,21 @@ fn subtype_set_dict(obj: PyObjectRef, value: PyObjectRef, vm: &VirtualMachine) -
}
}
// subtype_get_weakref
fn subtype_get_weakref(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
// Return the first weakref in the weakref list, or None
let weakref = obj.get_weakrefs();
Ok(weakref.unwrap_or_else(|| vm.ctx.none()))
}
// subtype_set_weakref: __weakref__ is read-only
fn subtype_set_weakref(obj: PyObjectRef, _value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
Err(vm.new_attribute_error(format!(
"attribute '__weakref__' of '{}' objects is not writable",
obj.class().name()
)))
}
/*
* The magical type type
*/
+1 -1
View File
@@ -98,7 +98,7 @@ impl PyUnion {
}
#[pyclass(
flags(DISALLOW_INSTANTIATION),
flags(DISALLOW_INSTANTIATION, HAS_WEAKREF),
with(Hashable, Comparable, AsMapping, AsNumber, Representable)
)]
impl PyUnion {
+10 -10
View File
@@ -123,6 +123,7 @@ impl PyMethodDef {
zelf: None,
value: self,
module: None,
_method_def_owner: None,
}
}
@@ -144,6 +145,7 @@ impl PyMethodDef {
zelf: Some(obj),
value: self,
module: None,
_method_def_owner: None,
},
class,
}
@@ -162,6 +164,7 @@ impl PyMethodDef {
zelf: Some(obj),
value: self,
module: None,
_method_def_owner: None,
};
PyRef::new_ref(
function,
@@ -217,6 +220,7 @@ impl PyMethodDef {
zelf: Some(class.to_owned().into()),
value: self,
module: None,
_method_def_owner: None,
};
PyNativeMethod { func, class }.into_ref(ctx)
}
@@ -293,14 +297,12 @@ impl Py<HeapMethodDef> {
}
pub fn build_function(&self, vm: &VirtualMachine) -> PyRef<PyNativeFunction> {
let function = unsafe { self.method() }.to_function();
let dict = vm.ctx.new_dict();
dict.set_item("__method_def__", self.to_owned().into(), vm)
.unwrap();
let mut function = unsafe { self.method() }.to_function();
function._method_def_owner = Some(self.to_owned().into());
PyRef::new_ref(
function,
vm.ctx.types.builtin_function_or_method_type.to_owned(),
Some(dict),
None,
)
}
@@ -309,14 +311,12 @@ impl Py<HeapMethodDef> {
class: &'static Py<PyType>,
vm: &VirtualMachine,
) -> PyRef<PyMethodDescriptor> {
let function = unsafe { self.method() }.to_method(class, &vm.ctx);
let dict = vm.ctx.new_dict();
dict.set_item("__method_def__", self.to_owned().into(), vm)
.unwrap();
let mut function = unsafe { self.method() }.to_method(class, &vm.ctx);
function._method_def_owner = Some(self.to_owned().into());
PyRef::new_ref(
function,
vm.ctx.types.method_descriptor_type.to_owned(),
Some(dict),
None,
)
}
}
+278 -62
View File
@@ -17,7 +17,7 @@ use super::{
};
use crate::object::traverse_object::PyObjVTable;
use crate::{
builtins::{PyDictRef, PyType, PyTypeRef},
builtins::{PyDict, PyDictRef, PyType, PyTypeRef},
common::{
atomic::{Ordering, PyAtomic, Radium},
linked_list::{Link, Pointers},
@@ -203,7 +203,8 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
false
};
if !pushed {
drop(unsafe { Box::from_raw(obj as *mut PyInner<T>) });
// Deallocate the object memory (handles ObjExt prefix if present)
unsafe { PyInner::dealloc(obj as *mut PyInner<T>) };
}
// Drop child references - may trigger recursive destruction.
@@ -286,6 +287,51 @@ unsafe impl Link for GcLink {
}
}
/// Extension fields for objects that need dict, weakref list, or member slots.
/// Allocated as a prefix before PyInner when needed (prefix allocation pattern).
/// Access via `PyInner::ext_ref()` using negative offset from the object pointer.
///
/// align(8) ensures size_of::<ObjExt>() is always a multiple of 8,
/// so the offset from Layout::extend equals size_of::<ObjExt>() for any
/// PyInner<T> alignment (important on wasm32 where pointers are 4 bytes
/// but some payloads like PyWeak have align 8 due to i64 fields).
#[repr(C, align(8))]
pub(super) struct ObjExt {
pub(super) dict: Option<InstanceDict>,
pub(super) weak_list: WeakRefList,
pub(super) slots: Box<[PyRwLock<Option<PyObjectRef>>]>,
}
impl ObjExt {
fn new(dict: Option<PyDictRef>, member_count: usize) -> Self {
Self {
dict: dict.map(InstanceDict::new),
weak_list: WeakRefList::new(),
slots: core::iter::repeat_with(|| PyRwLock::new(None))
.take(member_count)
.collect_vec()
.into_boxed_slice(),
}
}
}
impl fmt::Debug for ObjExt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[ObjExt]")
}
}
/// Precomputed offset from PyInner pointer back to ObjExt prefix.
/// ObjExt is #[repr(C, align(8))] and PyInner is #[repr(C)], so as long as
/// ObjExt's alignment >= PyInner's alignment, Layout::extend adds no padding
/// and the offset equals size_of::<ObjExt>().
const EXT_OFFSET: usize = core::mem::size_of::<ObjExt>();
// Guarantee: ObjExt size is a multiple of its alignment, and its alignment
// is >= any PyInner alignment, so Layout::extend produces no inter-padding.
const _: () =
assert!(core::mem::size_of::<ObjExt>().is_multiple_of(core::mem::align_of::<ObjExt>()));
const _: () = assert!(core::mem::align_of::<ObjExt>() >= core::mem::align_of::<PyInner<()>>());
/// 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.
@@ -302,14 +348,46 @@ pub(super) struct PyInner<T> {
pub(super) gc_pointers: Pointers<PyObject>,
pub(super) typ: PyAtomicRef<PyType>, // __class__ member
pub(super) dict: Option<InstanceDict>,
pub(super) weak_list: WeakRefList,
pub(super) slots: Box<[PyRwLock<Option<PyObjectRef>>]>,
pub(super) payload: T,
}
pub(crate) const SIZEOF_PYOBJECT_HEAD: usize = core::mem::size_of::<PyInner<()>>();
impl<T> PyInner<T> {
/// Check if this object has an ObjExt prefix based on type flags.
/// Uses raw pointer reads to avoid Stacked Borrows violations during bootstrap,
/// where type objects have self-referential typ pointers that may be mutated.
#[inline(always)]
fn has_ext(&self) -> bool {
// Read slots via raw pointers only — creating a &Py<PyType> reference
// would retag the entire object, conflicting with &mut writes during bootstrap.
let typ_ptr = self.typ.load_raw();
let slots = unsafe { core::ptr::addr_of!((*typ_ptr).0.payload.slots) };
let flags = unsafe { core::ptr::addr_of!((*slots).flags).read() };
let member_count = unsafe { core::ptr::addr_of!((*slots).member_count).read() };
flags.has_feature(crate::types::PyTypeFlags::HAS_DICT)
|| flags.has_feature(crate::types::PyTypeFlags::HAS_WEAKREF)
|| member_count > 0
}
/// Access the ObjExt prefix at a negative offset from this PyInner.
/// Returns None if this object was allocated without the prefix.
///
/// Uses exposed provenance to reconstruct a pointer covering the entire
/// allocation (ObjExt prefix + PyInner). The allocation pointer's provenance
/// is exposed at allocation time via `expose_provenance()`.
#[inline(always)]
pub(super) fn ext_ref(&self) -> Option<&ObjExt> {
if !self.has_ext() {
return None;
}
let self_addr = (self as *const Self as *const u8).addr();
let ext_ptr =
core::ptr::with_exposed_provenance::<ObjExt>(self_addr.wrapping_sub(EXT_OFFSET));
Some(unsafe { &*ext_ptr })
}
}
impl<T: fmt::Debug> fmt::Debug for PyInner<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[PyObject {:?}]", &self.payload)
@@ -764,7 +842,8 @@ impl PyWeak {
}
let obj = unsafe { &*obj_ptr };
let wrl = &obj.0.weak_list;
// Safety: if a weakref exists pointing to this object, ext must be present
let wrl = &obj.0.ext_ref().unwrap().weak_list;
// Compute our Py<PyWeak> node pointer from payload address
let offset = std::mem::offset_of!(PyInner<Self>, payload);
@@ -839,24 +918,93 @@ impl InstanceDict {
}
}
impl<T: PyPayload> PyInner<T> {
/// Deallocate a PyInner, handling the optional ObjExt prefix.
///
/// # Safety
/// `ptr` must be a valid pointer from `PyInner::new` and must not be used after this call.
unsafe fn dealloc(ptr: *mut Self) {
unsafe {
if (*ptr).has_ext() {
let ext_layout = core::alloc::Layout::new::<ObjExt>();
let inner_layout = core::alloc::Layout::new::<Self>();
let (combined, inner_offset) = ext_layout.extend(inner_layout).unwrap();
let combined = combined.pad_to_align();
let alloc_ptr = (ptr as *mut u8).sub(inner_offset);
core::ptr::drop_in_place(ptr);
core::ptr::drop_in_place(alloc_ptr as *mut ObjExt);
alloc::alloc::dealloc(alloc_ptr, combined);
} else {
drop(Box::from_raw(ptr));
}
}
}
}
impl<T: PyPayload + core::fmt::Debug> PyInner<T> {
fn new(payload: T, typ: PyTypeRef, dict: Option<PyDictRef>) -> Box<Self> {
/// Allocate a new PyInner, optionally with an ObjExt prefix.
/// Returns a raw pointer to the PyInner (NOT the allocation start).
/// For objects with ext, the allocation layout is: [ObjExt][PyInner<T>]
fn new(payload: T, typ: PyTypeRef, dict: Option<PyDictRef>) -> *mut Self {
let member_count = typ.slots.member_count;
Box::new(Self {
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(),
payload,
slots: core::iter::repeat_with(|| PyRwLock::new(None))
.take(member_count)
.collect_vec()
.into_boxed_slice(),
})
let needs_ext = typ
.slots
.flags
.has_feature(crate::types::PyTypeFlags::HAS_DICT)
|| typ
.slots
.flags
.has_feature(crate::types::PyTypeFlags::HAS_WEAKREF)
|| member_count > 0;
debug_assert!(
needs_ext || dict.is_none(),
"dict passed to type '{}' without HAS_DICT flag",
typ.name()
);
if needs_ext {
let ext_layout = core::alloc::Layout::new::<ObjExt>();
let inner_layout = core::alloc::Layout::new::<Self>();
let (combined, inner_offset) = ext_layout.extend(inner_layout).unwrap();
let combined = combined.pad_to_align();
let alloc_ptr = unsafe { alloc::alloc::alloc(combined) };
if alloc_ptr.is_null() {
alloc::alloc::handle_alloc_error(combined);
}
// Expose provenance so ext_ref() can reconstruct via with_exposed_provenance
alloc_ptr.expose_provenance();
unsafe {
let ext_ptr = alloc_ptr as *mut ObjExt;
ext_ptr.write(ObjExt::new(dict, member_count));
let inner_ptr = alloc_ptr.add(inner_offset) as *mut Self;
inner_ptr.write(Self {
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),
payload,
});
inner_ptr
}
} else {
Box::into_raw(Box::new(Self {
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),
payload,
}))
}
}
}
@@ -1075,9 +1223,30 @@ impl PyObjectRef {
}
impl PyObject {
/// Returns the WeakRefList if the object has ext (prefix allocation).
/// Note: This does NOT check HAS_WEAKREF flag. Callers creating weakrefs
/// must check HAS_WEAKREF themselves. This method is used in drop/GC paths
/// where the type may already be deallocated.
#[inline(always)]
const fn weak_ref_list(&self) -> Option<&WeakRefList> {
Some(&self.0.weak_list)
fn weak_ref_list(&self) -> Option<&WeakRefList> {
self.0.ext_ref().map(|ext| &ext.weak_list)
}
/// Returns the first weakref in the weakref list, if any.
pub(crate) fn get_weakrefs(&self) -> Option<PyObjectRef> {
let wrl = self.weak_ref_list()?;
let _lock = weakref_lock::lock(self as *const PyObject as usize);
let head_ptr = wrl.head.load(Ordering::Relaxed);
if head_ptr.is_null() {
None
} else {
let head = unsafe { &*head_ptr };
if head.0.ref_count.safe_inc() {
Some(unsafe { PyRef::from_raw(head_ptr) }.into())
} else {
None
}
}
}
pub(crate) fn downgrade_with_weakref_typ_opt(
@@ -1096,6 +1265,18 @@ impl PyObject {
typ: PyTypeRef,
vm: &VirtualMachine,
) -> PyResult<PyRef<PyWeak>> {
// Check HAS_WEAKREF flag first
if !self
.class()
.slots
.flags
.has_feature(crate::types::PyTypeFlags::HAS_WEAKREF)
{
return Err(vm.new_type_error(format!(
"cannot create weak reference to '{}' object",
self.class().name()
)));
}
let dict = if typ
.slots
.flags
@@ -1180,8 +1361,8 @@ impl PyObject {
}
#[inline(always)]
const fn instance_dict(&self) -> Option<&InstanceDict> {
self.0.dict.as_ref()
fn instance_dict(&self) -> Option<&InstanceDict> {
self.0.ext_ref().and_then(|ext| ext.dict.as_ref())
}
#[inline(always)]
@@ -1396,11 +1577,11 @@ impl PyObject {
}
pub(crate) fn get_slot(&self, offset: usize) -> Option<PyObjectRef> {
self.0.slots[offset].read().clone()
self.0.ext_ref().unwrap().slots[offset].read().clone()
}
pub(crate) fn set_slot(&self, offset: usize, value: Option<PyObjectRef>) {
*self.0.slots[offset].write() = value;
*self.0.ext_ref().unwrap().slots[offset].write() = value;
}
/// _PyObject_GC_IS_TRACKED
@@ -1486,10 +1667,18 @@ impl PyObject {
unsafe { clear_fn(ptr, &mut result) };
}
// 2. Clear member slots (subtype_clear)
for slot in obj.0.slots.iter() {
if let Some(val) = slot.write().take() {
result.push(val);
// 2. Clear dict and member slots (subtype_clear)
if let Some(ext) = obj.0.ext_ref() {
if let Some(dict) = ext.dict.as_ref() {
let dict_ref = dict.get();
// Clear dict entries to break cycles, then collect the dict itself
PyDict::clear(&dict_ref);
result.push(dict_ref.into());
}
for slot in ext.slots.iter() {
if let Some(val) = slot.write().take() {
result.push(val);
}
}
}
@@ -1513,7 +1702,11 @@ impl PyObject {
/// Check if this object has clear capability (tp_clear)
// Py_TPFLAGS_HAVE_GC types have tp_clear
pub fn gc_has_clear(&self) -> bool {
self.0.vtable.clear.is_some() || self.0.dict.is_some() || !self.0.slots.is_empty()
self.0.vtable.clear.is_some()
|| self
.0
.ext_ref()
.is_some_and(|ext| ext.dict.is_some() || !ext.slots.is_empty())
}
}
@@ -1902,7 +2095,7 @@ impl<T: PyPayload + crate::object::MaybeTraverse + core::fmt::Debug> PyRef<T> {
drop(typ);
unsafe { NonNull::new_unchecked(inner.cast::<Py<T>>()) }
} else {
let inner = Box::into_raw(PyInner::new(payload, typ, dict));
let inner = PyInner::new(payload, typ, dict);
unsafe { NonNull::new_unchecked(inner.cast::<Py<T>>()) }
};
@@ -2122,34 +2315,57 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) {
heaptype_ext: None,
tp_version_tag: core::sync::atomic::AtomicU32::new(0),
};
let type_type_ptr = Box::into_raw(Box::new(partially_init!(
PyInner::<PyType> {
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,
slots: Box::new([]),
},
Uninit { typ }
)));
let object_type_ptr = Box::into_raw(Box::new(partially_init!(
PyInner::<PyType> {
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,
slots: Box::new([]),
},
Uninit { typ },
)));
// Both type_type and object_type are instances of `type`, which has HAS_WEAKREF,
// so they need prefix allocation with ObjExt.
let alloc_type_with_ext = || -> *mut MaybeUninit<PyInner<PyType>> {
let ext_layout = core::alloc::Layout::new::<ObjExt>();
let inner_layout = core::alloc::Layout::new::<MaybeUninit<PyInner<PyType>>>();
let (combined, inner_offset) = ext_layout.extend(inner_layout).unwrap();
let combined = combined.pad_to_align();
let alloc_ptr = unsafe { alloc::alloc::alloc(combined) };
if alloc_ptr.is_null() {
alloc::alloc::handle_alloc_error(combined);
}
// Expose provenance so ext_ref() can reconstruct via with_exposed_provenance
alloc_ptr.expose_provenance();
unsafe {
let ext_ptr = alloc_ptr as *mut ObjExt;
ext_ptr.write(ObjExt::new(None, 0));
alloc_ptr.add(inner_offset) as *mut MaybeUninit<PyInner<PyType>>
}
};
let type_type_ptr = alloc_type_with_ext();
unsafe {
type_type_ptr.write(partially_init!(
PyInner::<PyType> {
ref_count: RefCount::new(),
vtable: PyObjVTable::of::<PyType>(),
gc_bits: Radium::new(0),
gc_generation: Radium::new(GC_UNTRACKED),
gc_pointers: Pointers::new(),
payload: type_payload,
},
Uninit { typ }
));
}
let object_type_ptr = alloc_type_with_ext();
unsafe {
object_type_ptr.write(partially_init!(
PyInner::<PyType> {
ref_count: RefCount::new(),
vtable: PyObjVTable::of::<PyType>(),
gc_bits: Radium::new(0),
gc_generation: Radium::new(GC_UNTRACKED),
gc_pointers: Pointers::new(),
payload: object_payload,
},
Uninit { typ },
));
}
let object_type_ptr = object_type_ptr as *mut PyInner<PyType>;
let type_type_ptr = type_type_ptr as *mut PyInner<PyType>;
+13 -1
View File
@@ -289,8 +289,12 @@ impl<T: fmt::Debug> fmt::Debug for PyAtomicRef<T> {
impl<T: PyPayload> From<PyRef<T>> for PyAtomicRef<T> {
fn from(pyref: PyRef<T>) -> Self {
let py = PyRef::leak(pyref);
let ptr = py as *const _ as *mut u8;
// Expose provenance so we can re-derive via with_exposed_provenance
// without Stacked Borrows tag restrictions during bootstrap
ptr.expose_provenance();
Self {
inner: Radium::new(py as *const _ as *mut _),
inner: Radium::new(ptr),
_phantom: Default::default(),
}
}
@@ -311,6 +315,14 @@ impl<T: PyPayload> Deref for PyAtomicRef<T> {
}
impl<T: PyPayload> PyAtomicRef<T> {
/// Load the raw pointer without creating a reference.
/// Avoids Stacked Borrows retag, safe for use during bootstrap
/// when type objects have self-referential pointers being mutated.
#[inline(always)]
pub(super) fn load_raw(&self) -> *const Py<T> {
self.inner.load(Ordering::Relaxed).cast::<Py<T>>()
}
/// # Safety
/// The caller is responsible to keep the returned PyRef alive
/// until no more reference can be used via PyAtomicRef::deref()
+11 -6
View File
@@ -65,9 +65,12 @@ unsafe impl Traverse for PyInner<Erased> {
let typ_obj: &PyObject = unsafe { &*(typ as *const _ as *const PyObject) };
tracer_fn(typ_obj);
}
self.dict.traverse(tracer_fn);
// weak_list is inline atomic pointers, no heap allocation, no trace
self.slots.traverse(tracer_fn);
// Traverse ObjExt prefix fields (dict and slots) if present
if let Some(ext) = self.ext_ref() {
ext.dict.traverse(tracer_fn);
// weak_list is atomic pointers, no trace needed
ext.slots.traverse(tracer_fn);
}
if let Some(f) = self.vtable.trace {
unsafe {
@@ -87,9 +90,11 @@ unsafe impl<T: MaybeTraverse> Traverse for PyInner<T> {
let typ_obj: &PyObject = unsafe { &*(typ as *const _ as *const PyObject) };
tracer_fn(typ_obj);
}
self.dict.traverse(tracer_fn);
// weak_list is inline atomic pointers, no heap allocation, no trace
self.slots.traverse(tracer_fn);
// Traverse ObjExt prefix fields (dict and slots) if present
if let Some(ext) = self.ext_ref() {
ext.dict.traverse(tracer_fn);
ext.slots.traverse(tracer_fn);
}
T::try_traverse(&self.payload, tracer_fn);
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ mod _collections {
}
#[pyclass(
flags(BASETYPE),
flags(BASETYPE, HAS_WEAKREF),
with(
Constructor,
Initializer,
+1 -1
View File
@@ -140,7 +140,7 @@ mod _functools {
#[pyclass(
with(Constructor, Callable, GetDescriptor, Representable),
flags(BASETYPE, HAS_DICT)
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl PyPartial {
#[pygetset]
+19 -13
View File
@@ -413,7 +413,10 @@ mod _io {
#[derive(Debug, Default, PyPayload)]
pub struct _IOBase;
#[pyclass(with(IterNext, Iterable, Destructor), flags(BASETYPE, HAS_DICT))]
#[pyclass(
with(IterNext, Iterable, Destructor),
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl _IOBase {
#[pymethod]
fn seek(
@@ -634,7 +637,7 @@ mod _io {
#[repr(transparent)]
pub(super) struct _RawIOBase(_IOBase);
#[pyclass(flags(BASETYPE, HAS_DICT))]
#[pyclass(flags(BASETYPE, HAS_DICT, HAS_WEAKREF))]
impl _RawIOBase {
#[pymethod]
fn read(instance: PyObjectRef, size: OptionalSize, vm: &VirtualMachine) -> PyResult {
@@ -720,7 +723,7 @@ mod _io {
#[repr(transparent)]
struct _BufferedIOBase(_IOBase);
#[pyclass(flags(BASETYPE))]
#[pyclass(flags(BASETYPE, HAS_WEAKREF))]
impl _BufferedIOBase {
#[pymethod]
fn read(zelf: PyObjectRef, _size: OptionalArg, vm: &VirtualMachine) -> PyResult {
@@ -785,7 +788,7 @@ mod _io {
#[repr(transparent)]
struct _TextIOBase(_IOBase);
#[pyclass(flags(BASETYPE))]
#[pyclass(flags(BASETYPE, HAS_WEAKREF))]
impl _TextIOBase {
#[pygetset]
fn encoding(_zelf: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
@@ -1981,7 +1984,7 @@ mod _io {
#[pyclass(
with(Constructor, BufferedMixin, BufferedReadable, Destructor),
flags(BASETYPE, HAS_DICT)
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl BufferedReader {}
@@ -2085,7 +2088,7 @@ mod _io {
#[pyclass(
with(Constructor, BufferedMixin, BufferedWritable, Destructor),
flags(BASETYPE, HAS_DICT)
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl BufferedWriter {}
@@ -2159,7 +2162,7 @@ mod _io {
BufferedWritable,
Destructor
),
flags(BASETYPE, HAS_DICT)
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl BufferedRandom {}
@@ -2229,7 +2232,7 @@ mod _io {
BufferedWritable,
Destructor
),
flags(BASETYPE, HAS_DICT)
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl BufferedRWPair {
#[pymethod]
@@ -3015,7 +3018,7 @@ mod _io {
IterNext,
Representable
),
flags(BASETYPE)
flags(BASETYPE, HAS_WEAKREF)
)]
impl TextIOWrapper {
#[pymethod]
@@ -4376,7 +4379,7 @@ mod _io {
}
}
#[pyclass(flags(BASETYPE, HAS_DICT), with(Constructor, Initializer))]
#[pyclass(flags(BASETYPE, HAS_DICT, HAS_WEAKREF), with(Constructor, Initializer))]
impl StringIO {
#[pymethod]
const fn readable(&self) -> bool {
@@ -4593,7 +4596,10 @@ mod _io {
}
}
#[pyclass(flags(BASETYPE, HAS_DICT), with(PyRef, Constructor, Initializer))]
#[pyclass(
flags(BASETYPE, HAS_DICT, HAS_WEAKREF),
with(PyRef, Constructor, Initializer)
)]
impl BytesIO {
#[pymethod]
const fn readable(&self) -> bool {
@@ -5634,7 +5640,7 @@ mod fileio {
#[pyclass(
with(Constructor, Initializer, Representable, Destructor),
flags(BASETYPE, HAS_DICT)
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl FileIO {
fn io_error(
@@ -6390,7 +6396,7 @@ mod winconsoleio {
#[pyclass(
with(Constructor, Initializer, Representable, Destructor),
flags(BASETYPE, HAS_DICT)
flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
)]
impl WindowsConsoleIO {
#[allow(dead_code)]
+1 -1
View File
@@ -667,7 +667,7 @@ mod decl {
groupby: PyRef<PyItertoolsGroupBy>,
}
#[pyclass(with(IterNext, Iterable))]
#[pyclass(with(IterNext, Iterable), flags(HAS_WEAKREF))]
impl PyItertoolsGrouper {}
impl SelfIter for PyItertoolsGrouper {}
+1 -1
View File
@@ -212,7 +212,7 @@ mod _sre {
};
}
#[pyclass(with(Hashable, Comparable, Representable))]
#[pyclass(with(Hashable, Comparable, Representable), flags(HAS_WEAKREF))]
impl Pattern {
fn with_str<F, R>(string: &PyObject, vm: &VirtualMachine, f: F) -> PyResult<R>
where
+2 -2
View File
@@ -132,7 +132,7 @@ pub(crate) mod _thread {
}
}
#[pyclass(with(Constructor, Representable))]
#[pyclass(with(Constructor, Representable), flags(HAS_WEAKREF))]
impl Lock {
#[pymethod]
#[pymethod(name = "acquire_lock")]
@@ -205,7 +205,7 @@ pub(crate) mod _thread {
}
}
#[pyclass(with(Representable), flags(BASETYPE))]
#[pyclass(with(Representable), flags(BASETYPE, HAS_WEAKREF))]
impl RLock {
#[pyslot]
fn slot_new(cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
+14 -5
View File
@@ -94,7 +94,10 @@ pub(crate) mod typevar {
contravariant: bool,
infer_variance: bool,
}
#[pyclass(flags(HAS_DICT), with(AsNumber, Constructor, Representable))]
#[pyclass(
flags(HAS_DICT, HAS_WEAKREF),
with(AsNumber, Constructor, Representable)
)]
impl TypeVar {
#[pymethod]
fn __mro_entries__(&self, _bases: PyObjectRef, vm: &VirtualMachine) -> PyResult {
@@ -461,7 +464,10 @@ pub(crate) mod typevar {
infer_variance: bool,
}
#[pyclass(flags(HAS_DICT), with(AsNumber, Constructor, Representable))]
#[pyclass(
flags(HAS_DICT, HAS_WEAKREF),
with(AsNumber, Constructor, Representable)
)]
impl ParamSpec {
#[pymethod]
fn __mro_entries__(&self, _bases: PyObjectRef, vm: &VirtualMachine) -> PyResult {
@@ -713,7 +719,10 @@ pub(crate) mod typevar {
default_value: PyMutex<PyObjectRef>,
evaluate_default: PyMutex<PyObjectRef>,
}
#[pyclass(flags(HAS_DICT), with(Constructor, Representable, Iterable))]
#[pyclass(
flags(HAS_DICT, HAS_WEAKREF),
with(Constructor, Representable, Iterable)
)]
impl TypeVarTuple {
#[pygetset]
fn __name__(&self) -> PyObjectRef {
@@ -883,7 +892,7 @@ pub(crate) mod typevar {
pub struct ParamSpecArgs {
__origin__: PyObjectRef,
}
#[pyclass(with(Constructor, Representable, Comparable))]
#[pyclass(with(Constructor, Representable, Comparable), flags(HAS_WEAKREF))]
impl ParamSpecArgs {
#[pymethod]
fn __mro_entries__(&self, _bases: PyObjectRef, vm: &VirtualMachine) -> PyResult {
@@ -946,7 +955,7 @@ pub(crate) mod typevar {
pub struct ParamSpecKwargs {
__origin__: PyObjectRef,
}
#[pyclass(with(Constructor, Representable, Comparable))]
#[pyclass(with(Constructor, Representable, Comparable), flags(HAS_WEAKREF))]
impl ParamSpecKwargs {
#[pymethod]
fn __mro_entries__(&self, _bases: PyObjectRef, vm: &VirtualMachine) -> PyResult {
+1
View File
@@ -228,6 +228,7 @@ bitflags! {
// This is not a stable API
const _MATCH_SELF = 1 << 22;
const HAS_DICT = 1 << 40;
const HAS_WEAKREF = 1 << 41;
#[cfg(debug_assertions)]
const _CREATED_WITH_FLAGS = 1 << 63;
+9 -1
View File
@@ -49,7 +49,15 @@ pub fn py_err_to_js_err(vm: &VirtualMachine, py_err: &Py<PyBaseException>) -> Js
serde_wasm_bindgen::to_value(&exceptions::SerializeException::new(vm, py_err));
match res {
Ok(err_info) => PyError::new(err_info).into(),
Err(e) => e.into(),
Err(_) => {
// Fallback: create a basic JS Error with the exception type and message
let exc_type = py_err.class().name().to_string();
let msg = match py_err.as_object().str(vm) {
Ok(s) => format!("{exc_type}: {s}"),
Err(_) => exc_type,
};
js_sys::Error::new(&msg).into()
}
}
}
}