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
Next Next commit
PySubclass
  • Loading branch information
youknowone committed Dec 14, 2025
commit e05cf537809d715464874cbea62929ecc95c5c5a
105 changes: 77 additions & 28 deletions crates/derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,18 @@ fn generate_class_def(
false
}
});
// Check if the type has #[repr(transparent)] - only then we can safely
// generate PySubclass impl (requires same memory layout as base type)
let is_repr_transparent = attrs.iter().any(|attr| {
attr.path().is_ident("repr")
&& if let Ok(Meta::List(l)) = attr.parse_meta() {
l.nested
.into_iter()
.any(|n| n.get_ident().is_some_and(|p| p == "transparent"))
} else {
false
}
});
if base.is_some() && is_pystruct {
bail_span!(ident, "PyStructSequence cannot have `base` class attr",);
}
Expand Down Expand Up @@ -379,12 +391,40 @@ fn generate_class_def(
}
});

let base_or_object = if let Some(base) = base {
let base_or_object = if let Some(ref base) = base {
quote! { #base }
} else {
quote! { ::rustpython_vm::builtins::PyBaseObject }
};

// Generate PySubclass impl for #[repr(transparent)] types with base class
// (tuple struct assumed, so &self.0 works)
let subclass_impl = if !is_pystruct && is_repr_transparent {
base.as_ref().map(|typ| {
quote! {
impl ::rustpython_vm::class::PySubclass for #ident {
type Base = #typ;

#[inline]
fn as_base(&self) -> &Self::Base {
&self.0
}
}
}
})
} else {
None
};

Comment on lines +485 to +503

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

PySubclass::as_base() assumes tuple structs (self.0) but validate_base_field also allows named-field structs.
For #[repr(transparent)] struct S { base: Base } this will generate invalid code. Either (a) restrict repr(transparent)+base to tuple structs, or (b) generate as_base() using the actual first field (named vs unnamed) discovered during validation.

🤖 Prompt for AI Agents
In crates/derive-impl/src/pyclass.rs around lines 485-503, the generated
PySubclass::as_base() unconditionally uses tuple struct access (&self.0) which
is invalid for named-field repr(transparent) structs; update the codegen to use
the actual first field determined during validation: have the validator record
whether the single field is named or unnamed and its identifier or index, then
emit as_base() using &self.<field_ident> for named fields and &self.0 for
unnamed tuple fields (alternatively, you may tighten validation to only allow
tuple structs and emit a clear compile error for named fields).

// Generate PySubclassTransparent marker for #[repr(transparent)] types
let transparent_impl = if !is_pystruct && is_repr_transparent && base.is_some() {
Some(quote! {
impl ::rustpython_vm::class::PySubclassTransparent for #ident {}
})
} else {
None
};

let tokens = quote! {
impl ::rustpython_vm::class::PyClassDef for #ident {
const NAME: &'static str = #name;
Expand All @@ -409,6 +449,9 @@ fn generate_class_def(

#base_class
}

#subclass_impl
#transparent_impl
};
Ok(tokens)
}
Expand All @@ -430,7 +473,7 @@ pub(crate) fn impl_pyclass(attr: PunctuatedNestedMeta, item: Item) -> Result<Tok
ident,
&class_name,
module_name.as_deref(),
base,
base.clone(),
metaclass,
unhashable,
attrs,
Expand Down Expand Up @@ -485,19 +528,46 @@ pub(crate) fn impl_pyclass(attr: PunctuatedNestedMeta, item: Item) -> Result<Tok
}
};

let impl_payload = if let Some(ctx_type_name) = class_meta.ctx_name()? {
let ctx_type_ident = Ident::new(&ctx_type_name, ident.span()); // FIXME span
// Generate PyPayload impl based on whether base exists
#[allow(clippy::collapsible_else_if)]
let impl_payload = if let Some(base_type) = &base {
let class_fn = if let Some(ctx_type_name) = class_meta.ctx_name()? {
let ctx_type_ident = Ident::new(&ctx_type_name, ident.span());
quote! { ctx.types.#ctx_type_ident }
} else {
quote! { <Self as ::rustpython_vm::class::StaticType>::static_type() }
};

// We need this to make extend mechanism work:
quote! {
impl ::rustpython_vm::PyPayload for #ident {
#[inline]
fn payload_type_id() -> ::std::any::TypeId {
<#base_type as ::rustpython_vm::PyPayload>::payload_type_id()
}

#[inline]
fn validate_downcastable_from(obj: &::rustpython_vm::PyObject) -> bool {
<Self as ::rustpython_vm::class::PyClassDef>::BASICSIZE <= obj.class().slots.basicsize && obj.class().fast_issubclass(<Self as ::rustpython_vm::class::StaticType>::static_type())

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I found another way to do with more tricks. Let's check it with OSError

}

fn class(ctx: &::rustpython_vm::vm::Context) -> &'static ::rustpython_vm::Py<::rustpython_vm::builtins::PyType> {
ctx.types.#ctx_type_ident
#class_fn
}
}
}
} else {
quote! {}
if let Some(ctx_type_name) = class_meta.ctx_name()? {
let ctx_type_ident = Ident::new(&ctx_type_name, ident.span());
quote! {
impl ::rustpython_vm::PyPayload for #ident {
fn class(ctx: &::rustpython_vm::vm::Context) -> &'static ::rustpython_vm::Py<::rustpython_vm::builtins::PyType> {
ctx.types.#ctx_type_ident
}
}
}
} else {
quote! {}
}
};

let empty_impl = if let Some(attrs) = class_meta.impl_attrs()? {
Expand Down Expand Up @@ -536,26 +606,6 @@ pub(crate) fn impl_pyexception(attr: PunctuatedNestedMeta, item: Item) -> Result
let class_name = class_meta.class_name()?;

let base_class_name = class_meta.base()?;
let impl_payload = if let Some(ctx_type_name) = class_meta.ctx_name()? {
let ctx_type_ident = Ident::new(&ctx_type_name, ident.span()); // FIXME span

// We need this to make extend mechanism work:
quote! {
impl ::rustpython_vm::PyPayload for #ident {
fn class(ctx: &::rustpython_vm::vm::Context) -> &'static ::rustpython_vm::Py<::rustpython_vm::builtins::PyType> {
ctx.exceptions.#ctx_type_ident
}
}
}
} else {
quote! {
impl ::rustpython_vm::PyPayload for #ident {
fn class(_ctx: &::rustpython_vm::vm::Context) -> &'static ::rustpython_vm::Py<::rustpython_vm::builtins::PyType> {
<Self as ::rustpython_vm::class::StaticType>::static_type()
}
}
}
};
let impl_pyclass = if class_meta.has_impl()? {
quote! {
#[pyexception]
Expand All @@ -568,7 +618,6 @@ pub(crate) fn impl_pyexception(attr: PunctuatedNestedMeta, item: Item) -> Result
let ret = quote! {
#[pyclass(module = false, name = #class_name, base = #base_class_name)]
#item
#impl_payload
#impl_pyclass
};
Ok(ret)
Expand Down
15 changes: 4 additions & 11 deletions crates/vm/src/builtins/bool.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use super::{PyInt, PyStrRef, PyType, PyTypeRef};
use crate::common::format::FormatSpec;
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyResult, TryFromBorrowedObject,
VirtualMachine,
AsObject, Context, Py, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, VirtualMachine,
class::PyClassImpl,
convert::{IntoPyException, ToPyObject, ToPyResult},
function::{FuncArgs, OptionalArg},
Expand Down Expand Up @@ -81,15 +80,9 @@ impl PyObjectRef {
}
}

#[pyclass(name = "bool", module = false, base = PyInt)]
pub struct PyBool;

impl PyPayload for PyBool {
#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.bool_type
}
}
#[pyclass(name = "bool", module = false, base = PyInt, ctx = "bool_type")]
#[repr(transparent)]
pub struct PyBool(pub PyInt);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we have to manually define the base layout to be always leading field.
The problem is Rust actually only guarantees it to be actually same layout only when #[repr(transparent)] is used.

What I didn't check yet: Can using #[repr(C)] helps? Is there any other repr options to keep the first field as same as its original layout?


impl Debug for PyBool {
fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result {
Expand Down
28 changes: 28 additions & 0 deletions crates/vm/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,31 @@ pub trait PyClassImpl: PyClassDef {
slots
}
}

/// 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.
///
/// For subclasses with `#[repr(transparent)]`, see also [`PySubclassTransparent`]
/// which enables ownership transfer via `into_base()`.
pub trait PySubclass: crate::PyPayload {
type Base: crate::PyPayload;

/// Returns a reference to the base type's payload.
fn as_base(&self) -> &Self::Base;
}

/// Marker trait for `#[repr(transparent)]` subclasses.
///
/// This trait enables ownership transfer from `PyRef<Self>` to `PyRef<Self::Base>`
/// via the `into_base_ref()` method. Only types with identical memory layout to their
/// base type (i.e., `#[repr(transparent)]` newtypes) should implement this trait.
///
/// # Safety
///
/// Implementors must ensure:
/// - The type uses `#[repr(transparent)]` with the Base type as the only field
/// - Memory layout is identical to the Base type
pub trait PySubclassTransparent: PySubclass {}
13 changes: 13 additions & 0 deletions crates/vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,19 @@ impl<T: PyPayload + std::fmt::Debug> PyRef<T> {
}
}

impl<T: crate::class::PySubclassTransparent + std::fmt::Debug> PyRef<T>
where
T::Base: std::fmt::Debug,
{
/// Converts this reference to the base type (ownership transfer).
/// Only available for `#[repr(transparent)]` types.
#[inline]
pub fn into_base_ref(self) -> PyRef<T::Base> {
// SAFETY: #[repr(transparent)] guarantees same memory layout
unsafe { std::mem::transmute(self) }
}
}
Comment on lines +1076 to +1103

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unsafe unreachable_unchecked could cause UB if invariants are violated.

Both into_base() and upcast() use unsafe { std::hint::unreachable_unchecked() } on downcast failure. While the type system constraints should prevent misuse, if invariants are violated (e.g., due to macro bugs or incorrect implementations), this will cause undefined behavior rather than a clear panic.

Consider using unreachable!() with a descriptive message for safer error handling, or at minimum document why this is safe:

     pub fn into_base(self) -> PyRef<T::Base> {
         let obj: PyObjectRef = self.into();
         match obj.downcast() {
             Ok(base_ref) => base_ref,
-            Err(_) => unsafe { std::hint::unreachable_unchecked() },
+            Err(_) => unreachable!("PySubclass invariant violated: into_base downcast failed"),
         }
     }
     #[inline]
     pub fn upcast<U: PyPayload + StaticType>(self) -> PyRef<U>
     where
         T: StaticType,
     {
         debug_assert!(T::static_type().is_subtype(U::static_type()));
         let obj: PyObjectRef = self.into();
         match obj.downcast::<U>() {
             Ok(upcast_ref) => upcast_ref,
-            Err(_) => unsafe { std::hint::unreachable_unchecked() },
+            Err(_) => unreachable!("PySubclass invariant violated: upcast to {} failed", std::any::type_name::<U>()),
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
impl<T: crate::class::PySubclass + std::fmt::Debug> PyRef<T>
where
T::Base: std::fmt::Debug,
{
/// Converts this reference to the base type (ownership transfer).
/// # Safety
/// T and T::Base must have compatible layouts in size_of::<T::Base>() bytes.
#[inline]
pub fn into_base(self) -> PyRef<T::Base> {
let obj: PyObjectRef = self.into();
match obj.downcast() {
Ok(base_ref) => base_ref,
Err(_) => unsafe { std::hint::unreachable_unchecked() },
}
}
#[inline]
pub fn upcast<U: PyPayload + StaticType>(self) -> PyRef<U>
where
T: StaticType,
{
debug_assert!(T::static_type().is_subtype(U::static_type()));
let obj: PyObjectRef = self.into();
match obj.downcast::<U>() {
Ok(upcast_ref) => upcast_ref,
Err(_) => unsafe { std::hint::unreachable_unchecked() },
}
}
}
impl<T: crate::class::PySubclass + std::fmt::Debug> PyRef<T>
where
T::Base: std::fmt::Debug,
{
/// Converts this reference to the base type (ownership transfer).
/// # Safety
/// T and T::Base must have compatible layouts in size_of::<T::Base>() bytes.
#[inline]
pub fn into_base(self) -> PyRef<T::Base> {
let obj: PyObjectRef = self.into();
match obj.downcast() {
Ok(base_ref) => base_ref,
Err(_) => unreachable!("PySubclass invariant violated: into_base downcast failed"),
}
}
#[inline]
pub fn upcast<U: PyPayload + StaticType>(self) -> PyRef<U>
where
T: StaticType,
{
debug_assert!(T::static_type().is_subtype(U::static_type()));
let obj: PyObjectRef = self.into();
match obj.downcast::<U>() {
Ok(upcast_ref) => upcast_ref,
Err(_) => unreachable!("PySubclass invariant violated: upcast to {} failed", std::any::type_name::<U>()),
}
}
}
🤖 Prompt for AI Agents
In crates/vm/src/object/core.rs around lines 1076 to 1103, the methods
into_base() and upcast() call unsafe { std::hint::unreachable_unchecked() } when
a downcast fails, which can produce undefined behavior if invariants are ever
violated; replace the unreachable_unchecked() calls with a safe panic (e.g.,
unreachable!() or panic! with a descriptive message) so a violation yields a
clear panic and backtrace rather than UB, and add a short comment above each
branch documenting the invariant that guarantees the downcast should succeed (or
keep a debug_assert verifying the subtype relation before the downcast).


impl<T> Borrow<PyObject> for PyRef<T>
where
T: PyPayload,
Expand Down
20 changes: 10 additions & 10 deletions crates/vm/src/object/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ cfg_if::cfg_if! {
}
}

#[cold]
pub(crate) fn cold_downcast_type_error(
vm: &VirtualMachine,
class: &Py<PyType>,
obj: &PyObject,
) -> PyBaseExceptionRef {
vm.new_downcast_type_error(class, obj)
}

pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static {
#[inline]
fn payload_type_id() -> std::any::TypeId {
Expand All @@ -38,17 +47,8 @@ pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static {
return Ok(());
}

#[cold]
fn raise_downcast_type_error(
vm: &VirtualMachine,
class: &Py<PyType>,
obj: &PyObject,
) -> PyBaseExceptionRef {
vm.new_downcast_type_error(class, obj)
}

let class = Self::class(&vm.ctx);
Err(raise_downcast_type_error(vm, class, obj))
Err(cold_downcast_type_error(vm, class, obj))
}

fn class(ctx: &Context) -> &'static Py<PyType>;
Expand Down
3 changes: 1 addition & 2 deletions crates/vm/src/stdlib/ctypes/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use rustpython_vm::stdlib::ctypes::base::PyCData;
/// PyCArrayType - metatype for Array types
/// CPython stores array info (type, length) in StgInfo via type_data
#[pyclass(name = "PyCArrayType", base = PyType, module = "_ctypes")]
#[derive(Debug, Default, PyPayload)]
#[derive(Debug, Default)]
pub struct PyCArrayType {}

/// Create a new Array type with StgInfo stored in type_data (CPython style)
Expand Down Expand Up @@ -235,7 +235,6 @@ impl AsNumber for PyCArrayType {
metaclass = "PyCArrayType",
module = "_ctypes"
)]
#[derive(PyPayload)]
pub struct PyCArray {
/// Element type - can be a simple type (c_int) or an array type (c_int * 5)
pub(super) typ: PyRwLock<PyObjectRef>,
Expand Down
3 changes: 1 addition & 2 deletions crates/vm/src/stdlib/ctypes/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ impl PyCData {
}

#[pyclass(module = "_ctypes", name = "PyCSimpleType", base = PyType)]
#[derive(Debug, PyPayload, Default)]
#[derive(Debug, Default)]
pub struct PyCSimpleType {}

#[pyclass(flags(BASETYPE), with(AsNumber))]
Expand Down Expand Up @@ -411,7 +411,6 @@ impl AsNumber for PyCSimpleType {
base = PyCData,
metaclass = "PyCSimpleType"
)]
#[derive(PyPayload)]
pub struct PyCSimple {
pub _type_: String,
pub value: AtomicCell<PyObjectRef>,
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/stdlib/ctypes/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use super::structure::PyCStructure;
use super::union::PyCUnion;

#[pyclass(name = "PyCFieldType", base = PyType, module = "_ctypes")]
#[derive(PyPayload, Debug)]
#[derive(Debug)]
pub struct PyCFieldType {
#[allow(dead_code)]
pub(super) inner: PyCField,
Expand Down
1 change: 0 additions & 1 deletion crates/vm/src/stdlib/ctypes/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ impl ReturnType for PyNone {
}

#[pyclass(module = "_ctypes", name = "CFuncPtr", base = PyCData)]
#[derive(PyPayload)]
pub struct PyCFuncPtr {
pub name: PyRwLock<Option<String>>,
pub ptr: PyRwLock<Option<CodePtr>>,
Expand Down
4 changes: 2 additions & 2 deletions crates/vm/src/stdlib/ctypes/pointer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::types::{AsNumber, Constructor};
use crate::{AsObject, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine};

#[pyclass(name = "PyCPointerType", base = PyType, module = "_ctypes")]
#[derive(PyPayload, Debug, Default)]
#[derive(Debug, Default)]
pub struct PyCPointerType {}

#[pyclass(flags(IMMUTABLETYPE), with(AsNumber))]
Expand Down Expand Up @@ -60,7 +60,7 @@ impl AsNumber for PyCPointerType {
metaclass = "PyCPointerType",
module = "_ctypes"
)]
#[derive(Debug, PyPayload)]
#[derive(Debug)]
pub struct PyCPointer {
contents: PyRwLock<PyObjectRef>,
}
Expand Down
3 changes: 1 addition & 2 deletions crates/vm/src/stdlib/ctypes/structure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use std::fmt::Debug;

/// PyCStructType - metaclass for Structure
#[pyclass(name = "PyCStructType", base = PyType, module = "_ctypes")]
#[derive(Debug, PyPayload, Default)]
#[derive(Debug, Default)]
pub struct PyCStructType {}

impl Constructor for PyCStructType {
Expand Down Expand Up @@ -218,7 +218,6 @@ pub struct FieldInfo {
base = PyCData,
metaclass = "PyCStructType"
)]
#[derive(PyPayload)]
pub struct PyCStructure {
/// Common CDataObject for memory buffer
pub(super) cdata: PyRwLock<CDataObject>,
Expand Down
3 changes: 1 addition & 2 deletions crates/vm/src/stdlib/ctypes/union.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use rustpython_common::lock::PyRwLock;

/// PyCUnionType - metaclass for Union
#[pyclass(name = "UnionType", base = PyType, module = "_ctypes")]
#[derive(Debug, PyPayload, Default)]
#[derive(Debug, Default)]
pub struct PyCUnionType {}

impl Constructor for PyCUnionType {
Expand Down Expand Up @@ -121,7 +121,6 @@ impl PyCUnionType {}

/// PyCUnion - base class for Union
#[pyclass(module = "_ctypes", name = "Union", base = PyCData, metaclass = "PyCUnionType")]
#[derive(PyPayload)]
pub struct PyCUnion {
/// Common CDataObject for memory buffer
pub(super) cdata: PyRwLock<CDataObject>,
Expand Down
Loading