diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index b935420591f..b82082bad5d 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -649,7 +649,9 @@ fn generate_class_def( let subclass_impl = if !is_pystruct && is_repr_transparent { base.as_ref().map(|typ| { quote! { - impl ::rustpython_vm::class::PySubclass for #ident { + // SAFETY: the transparent base field has the same layout as Self; + // pyclass also checks the object payload offsets and alignment. + unsafe impl ::rustpython_vm::class::PySubclass for #ident { type Base = #typ; #[inline] diff --git a/crates/derive-impl/src/pystructseq.rs b/crates/derive-impl/src/pystructseq.rs index aa89b3717bb..d4352735790 100644 --- a/crates/derive-impl/src/pystructseq.rs +++ b/crates/derive-impl/src/pystructseq.rs @@ -602,7 +602,8 @@ pub(crate) fn impl_pystruct_sequence( } // PySubclass for proper inheritance - impl ::rustpython_vm::class::PySubclass for #pytype_ident { + // SAFETY: the generated payload is a transparent wrapper around PyTuple. + unsafe impl ::rustpython_vm::class::PySubclass for #pytype_ident { type Base = ::rustpython_vm::builtins::PyTuple; #[inline] diff --git a/crates/vm/src/class.rs b/crates/vm/src/class.rs index 4afdf6329fb..fe655968665 100644 --- a/crates/vm/src/class.rs +++ b/crates/vm/src/class.rs @@ -451,13 +451,38 @@ pub trait PyClassImpl: PyClassDef { /// Trait for Python subclasses that can provide a reference to their base type. /// -/// This trait is automatically implemented by the `#[pyclass]` macro when -/// `base = SomeType` is specified. It provides safe reference access to the -/// base type's payload. +/// The `#[pyclass]` macro implements this trait for `#[repr(transparent)]` +/// subclasses with `base = SomeType`. Other native subclasses can implement +/// it manually when their base is a compatible physical prefix. /// -/// For subclasses with `#[repr(transparent)]` -/// which enables ownership transfer via `into_base()`. -pub trait PySubclass: crate::PyPayload { +/// # Safety +/// +/// Every valid `Self` must contain a valid `Base` at offset zero, returned by +/// `as_base`. Their payload offsets in `Py` and `Py` must match, +/// and `Py` must meet `Py`'s alignment requirement. The base must +/// remain valid for the lifetime of `Self`, independently of the object's +/// Python class or MRO. These guarantees allow safe borrowing and ownership +/// transfer of the same allocation as `Py`. +/// +/// A manual implementation must explicitly acknowledge this contract: +/// +/// ```compile_fail,E0200 +/// use rustpython_vm::{builtins::PyInt, class::PySubclass, pyclass, PyPayload}; +/// +/// #[pyclass(module = false, name = "IntWrapper")] +/// #[derive(Debug, PyPayload)] +/// #[repr(transparent)] +/// struct IntWrapper(PyInt); +/// +/// #[pyclass] +/// impl IntWrapper {} +/// +/// impl PySubclass for IntWrapper { +/// type Base = PyInt; +/// fn as_base(&self) -> &PyInt { &self.0 } +/// } +/// ``` +pub unsafe trait PySubclass: crate::PyPayload { type Base: crate::PyPayload; /// Returns a reference to the base type's payload. diff --git a/crates/vm/src/exception_group.rs b/crates/vm/src/exception_group.rs index 0f1b4dca994..8c182d98158 100644 --- a/crates/vm/src/exception_group.rs +++ b/crates/vm/src/exception_group.rs @@ -65,7 +65,8 @@ pub(super) mod types { excs_str: PyAtomicRef>, } - impl crate::class::PySubclass for PyBaseExceptionGroup { + // SAFETY: pyexception checks the base field offset, payload offset, and alignment. + unsafe impl crate::class::PySubclass for PyBaseExceptionGroup { type Base = PyBaseException; fn as_base(&self) -> &Self::Base { &self.base diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index 9562ad7a8b5..dbbdb920422 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -1750,7 +1750,8 @@ pub(super) mod types { code: PyAtomicRef>, } - impl crate::class::PySubclass for PySystemExit { + // SAFETY: pyexception checks the base field offset, payload offset, and alignment. + unsafe impl crate::class::PySubclass for PySystemExit { type Base = PyBaseException; fn as_base(&self) -> &Self::Base { &self.base @@ -1830,7 +1831,8 @@ pub(super) mod types { value: PyAtomicRef>, } - impl crate::class::PySubclass for PyStopIteration { + // SAFETY: pyexception checks the base field offset, payload offset, and alignment. + unsafe impl crate::class::PySubclass for PyStopIteration { type Base = PyException; fn as_base(&self) -> &Self::Base { &self.base @@ -1925,7 +1927,8 @@ pub(super) mod types { obj: PyAtomicRef>, } - impl crate::class::PySubclass for PyAttributeError { + // SAFETY: pyexception checks the base field offset, payload offset, and alignment. + unsafe impl crate::class::PySubclass for PyAttributeError { type Base = PyException; fn as_base(&self) -> &Self::Base { &self.base @@ -2051,7 +2054,8 @@ pub(super) mod types { name_from: PyAtomicRef>, } - impl crate::class::PySubclass for PyImportError { + // SAFETY: pyexception checks the base field offset, payload offset, and alignment. + unsafe impl crate::class::PySubclass for PyImportError { type Base = PyException; fn as_base(&self) -> &Self::Base { &self.base @@ -2263,7 +2267,8 @@ pub(super) mod types { name: PyAtomicRef>, } - impl crate::class::PySubclass for PyNameError { + // SAFETY: pyexception checks the base field offset, payload offset, and alignment. + unsafe impl crate::class::PySubclass for PyNameError { type Base = PyException; fn as_base(&self) -> &Self::Base { &self.base @@ -2357,7 +2362,8 @@ pub(super) mod types { written: AtomicCell, } - impl crate::class::PySubclass for PyOSError { + // SAFETY: pyexception checks the base field offset, payload offset, and alignment. + unsafe impl crate::class::PySubclass for PyOSError { type Base = PyException; fn as_base(&self) -> &Self::Base { &self.base @@ -2927,7 +2933,8 @@ pub(super) mod types { metadata: PyAtomicRef>, } - impl crate::class::PySubclass for PySyntaxError { + // SAFETY: pyexception checks the base field offset, payload offset, and alignment. + unsafe impl crate::class::PySubclass for PySyntaxError { type Base = PyException; fn as_base(&self) -> &Self::Base { &self.base @@ -3230,7 +3237,8 @@ pub(super) mod types { reason: PyAtomicRef>, } - impl crate::class::PySubclass for PyUnicodeError { + // SAFETY: pyexception checks the base field offset, payload offset, and alignment. + unsafe impl crate::class::PySubclass for PyUnicodeError { type Base = PyValueError; fn as_base(&self) -> &Self::Base { &self.base