Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
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
19 changes: 18 additions & 1 deletion crates/derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -669,9 +669,21 @@ fn generate_class_def(
checks: member_checks,
} = extras.member_table;

let native_layout = if is_repr_transparent {
base.as_ref().map(|base| {
quote! {
const NATIVE_LAYOUT_ID: ::core::any::TypeId =
<#base as ::rustpython_vm::class::PyClassDef>::NATIVE_LAYOUT_ID;
}
})
} else {
None
};

let tokens = quote! {
impl ::rustpython_vm::class::PyClassDef for #ident {
const NAME: &'static str = #name;
#native_layout
const MODULE_NAME: Option<&'static str> = #module_name;
const TP_NAME: &'static str = #module_class_name;
const DOC: ::rustpython_vm::function::ItemDoc = #doc;
Expand Down Expand Up @@ -903,9 +915,14 @@ pub(crate) fn impl_pyclass(attr: PunctuatedNestedMeta, item: Item) -> Result<Tok
impl ::rustpython_vm::PyPayload for #ident {
const PAYLOAD_TYPE_ID: ::core::any::TypeId = <#base_type as ::rustpython_vm::PyPayload>::PAYLOAD_TYPE_ID;

fn supports_native_layout(layout: ::core::any::TypeId) -> bool {
layout == <Self as ::rustpython_vm::class::PyClassDef>::NATIVE_LAYOUT_ID
|| <#base_type as ::rustpython_vm::PyPayload>::supports_native_layout(layout)
}

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

fn class(ctx: &::rustpython_vm::vm::Context) -> &'static ::rustpython_vm::Py<::rustpython_vm::builtins::PyType> {
Expand Down
8 changes: 7 additions & 1 deletion crates/derive-impl/src/pystructseq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ pub(crate) fn impl_pystruct_sequence(
const ATTR_DOCS: &'static [&'static str] = #attr_names;
}}
const BASICSIZE: usize = 0;
const NATIVE_LAYOUT_ID: ::core::any::TypeId = ::core::any::TypeId::of::<::rustpython_vm::builtins::PyTuple>();
const UNHASHABLE: bool = false;

type Base = ::rustpython_vm::builtins::PyTuple;
Expand All @@ -577,9 +578,14 @@ pub(crate) fn impl_pystruct_sequence(
impl ::rustpython_vm::PyPayload for #pytype_ident {
const PAYLOAD_TYPE_ID: ::core::any::TypeId = <::rustpython_vm::builtins::PyTuple as ::rustpython_vm::PyPayload>::PAYLOAD_TYPE_ID;

fn supports_native_layout(layout: ::core::any::TypeId) -> bool {
<::rustpython_vm::builtins::PyTuple as ::rustpython_vm::PyPayload>::supports_native_layout(layout)
}

#[inline]
unsafe fn validate_downcastable_from(obj: &::rustpython_vm::PyObject) -> bool {
obj.class().fast_issubclass(<Self as ::rustpython_vm::class::StaticType>::static_type())
obj.supports_native_layout(::core::any::TypeId::of::<::rustpython_vm::builtins::PyTuple>())
&& obj.class().fast_issubclass(<Self as ::rustpython_vm::class::StaticType>::static_type())
}

fn class(_ctx: &::rustpython_vm::vm::Context) -> &'static ::rustpython_vm::Py<::rustpython_vm::builtins::PyType> {
Expand Down
21 changes: 20 additions & 1 deletion crates/vm/src/builtins/builtin_func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,31 @@ impl PyNativeFunction {
}

// PyCMethodObject in CPython
#[pyclass(name = "builtin_function_or_method", module = false, base = PyNativeFunction, ctx = "builtin_function_or_method_type")]
#[pyclass(name = "builtin_function_or_method", module = false, base = PyNativeFunction, ctx = "builtin_function_or_method_type", payload = "manual")]
pub struct PyNativeMethod {
pub(crate) func: PyNativeFunction,
pub(crate) class: &'static Py<PyType>, // TODO: the actual life is &'self
}

impl PyPayload for PyNativeMethod {
const PAYLOAD_TYPE_ID: core::any::TypeId = PyNativeFunction::PAYLOAD_TYPE_ID;

fn supports_native_layout(layout: core::any::TypeId) -> bool {
layout == core::any::TypeId::of::<Self>()
|| PyNativeFunction::supports_native_layout(layout)
}

unsafe fn validate_downcastable_from(obj: &PyObject) -> bool {
// Both payloads use builtin_function_or_method as their Python class.
// The immutable allocation vtable distinguishes the larger CMethod.
obj.supports_native_layout(core::any::TypeId::of::<Self>())
}

fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.builtin_function_or_method_type
}
}

// 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.
Expand Down
4 changes: 4 additions & 0 deletions crates/vm/src/builtins/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2101,6 +2101,10 @@ impl MaybeTraverse for PyUtf8Str {
}

impl PyPayload for PyUtf8Str {
fn supports_native_layout(layout: core::any::TypeId) -> bool {
PyStr::supports_native_layout(layout)
}

#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.str_type
Expand Down
6 changes: 5 additions & 1 deletion crates/vm/src/builtins/type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,9 @@ impl PyType {
ctx: &Context,
defer_mro: bool,
) -> Result<PyRef<Self>, String> {
if slots.native_layout_id.is_none() {
slots.native_layout_id = base.slots.native_layout_id;
}
let mro = if defer_mro {
// Leave tp_mro unset so a custom metaclass mro() sees __mro__ is None.
Vec::new()
Expand Down Expand Up @@ -3798,7 +3801,8 @@ pub(crate) fn compatible_for_assignment(
(None, None) => true,
_ => false,
};
let compatible = newbase.is(oldbase) || (bases_equal && same_slots_added(newbase, oldbase));
let compatible = old_to.slots.native_layout_id == new_to.slots.native_layout_id
&& (newbase.is(oldbase) || (bases_equal && same_slots_added(newbase, oldbase)));
if compatible {
return Ok(());
}
Expand Down
6 changes: 5 additions & 1 deletion crates/vm/src/builtins/weakproxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ pub struct PyWeakProxy(PyWeak);
impl PyPayload for PyWeakProxy {
const PAYLOAD_TYPE_ID: core::any::TypeId = <PyWeak as PyPayload>::PAYLOAD_TYPE_ID;

fn supports_native_layout(layout: core::any::TypeId) -> bool {
PyWeak::supports_native_layout(layout)
}

#[inline]
unsafe fn validate_downcastable_from(obj: &PyObject) -> bool {
<Self as ::rustpython_vm::class::PyClassDef>::BASICSIZE <= obj.class().slots().basicsize
obj.supports_native_layout(core::any::TypeId::of::<Self>())
&& obj
.class()
.fast_issubclass(<Self as ::rustpython_vm::class::StaticType>::static_type())
Expand Down
6 changes: 6 additions & 0 deletions crates/vm/src/builtins/weakref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ pub struct WeakNewArgs {
}

impl PyPayload for PyWeak {
fn supports_native_layout(layout: core::any::TypeId) -> bool {
// Proxies are transparent views of the same PyWeak allocation.
layout == core::any::TypeId::of::<Self>()
|| layout == core::any::TypeId::of::<super::PyWeakProxy>()
}

#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.weakref_type
Expand Down
4 changes: 3 additions & 1 deletion crates/vm/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ pub trait StaticType {
}
}

pub trait PyClassDef {
pub trait PyClassDef: 'static {
const NAME: &'static str;
const MODULE_NAME: Option<&'static str>;
const TP_NAME: &'static str;
Expand All @@ -175,6 +175,7 @@ pub trait PyClassDef {
const ATTR_DOCS: &'static [&'static str] = &[];
const BASICSIZE: usize;
const ITEMSIZE: usize = 0;
const NATIVE_LAYOUT_ID: core::any::TypeId = core::any::TypeId::of::<Self>();
const UNHASHABLE: bool = false;
const MEMBERS: &'static [crate::builtins::descriptor::PyMemberSpec] = &[];

Expand Down Expand Up @@ -427,6 +428,7 @@ pub trait PyClassImpl: PyClassDef {
flags: crate::types::AtomicPyTypeFlags::from_plain(Self::TP_FLAGS),
name: Self::TP_NAME,
basicsize: Self::BASICSIZE,
native_layout_id: Some(Self::NATIVE_LAYOUT_ID),
itemsize: Self::ITEMSIZE,
doc: {
let internal = Self::INTERNAL_DOC;
Expand Down
5 changes: 5 additions & 0 deletions crates/vm/src/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ impl PyPayload for PyBaseException {

impl PyPayload for PyMemoryError {
const PAYLOAD_TYPE_ID: core::any::TypeId = <PyException as PyPayload>::PAYLOAD_TYPE_ID;

fn supports_native_layout(layout: core::any::TypeId) -> bool {
<PyException as PyPayload>::supports_native_layout(layout)
}

const HAS_FREELIST: bool = true;
const MAX_FREELIST: usize = MEMORY_ERROR_FREELIST_SIZE;

Expand Down
141 changes: 118 additions & 23 deletions crates/vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1668,13 +1668,8 @@ impl PyObjectRef {
/// another downcast can be attempted without unnecessary cloning.
#[inline]
pub fn downcast_exact<T: PyPayload>(self, vm: &VirtualMachine) -> Result<PyRefExact<T>, Self> {
if self.class().is(T::class(&vm.ctx)) {
// TODO: is this always true?
assert!(
self.downcastable::<T>(),
"obj.__class__ is T::class() but payload is not T"
);
// SAFETY: just asserted that downcastable::<T>()
if self.class().is(T::class(&vm.ctx)) && self.downcastable::<T>() {
// SAFETY: the class and allocation both match T.
Ok(unsafe { PyRefExact::new_unchecked(PyRef::from_obj_unchecked(self)) })
} else {
Err(self)
Expand Down Expand Up @@ -1779,7 +1774,13 @@ impl PyObject {
#[deprecated(note = "use downcastable instead")]
#[inline(always)]
pub fn payload_is<T: PyPayload>(&self) -> bool {
self.0.vtable.typeid == T::PAYLOAD_TYPE_ID
self.downcastable::<T>()
}

#[doc(hidden)]
#[must_use]
pub fn supports_native_layout(&self, layout: core::any::TypeId) -> bool {
(self.0.vtable.supports_native_layout)(layout)
}

/// Force to return payload as T.
Expand Down Expand Up @@ -1936,7 +1937,8 @@ impl PyObject {
pub fn downcast_ref_if_exact<T: PyPayload>(&self, vm: &VirtualMachine) -> Option<&Py<T>> {
self.class()
.is(T::class(&vm.ctx))
.then(|| unsafe { self.downcast_unchecked_ref::<T>() })
.then(|| self.downcast_ref::<T>())
.flatten()
}

/// # Safety
Expand Down Expand Up @@ -2875,32 +2877,27 @@ where
#[inline]
#[must_use]
pub fn into_base(self) -> PyRef<T::Base> {
let obj: PyObjectRef = self.into();
match obj.downcast() {
Ok(base_ref) => base_ref,
Err(_) => unsafe { core::hint::unreachable_unchecked() },
}
// PySubclass establishes the physical prefix; Python may have changed
// the object's class or MRO since this typed reference was constructed.
let ptr = self.ptr.cast();
core::mem::forget(self);
PyRef { ptr }
}
#[inline]
#[must_use]
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 { core::hint::unreachable_unchecked() },
}
obj.downcast::<U>().expect("invalid native upcast")
}
}

impl<T: crate::class::PySubclass> Py<T> {
/// Converts `&Py<T>` to `&Py<T::Base>`.
#[inline]
pub fn to_base(&self) -> &Py<T::Base> {
debug_assert!(self.as_object().downcast_ref::<T::Base>().is_some());
// SAFETY: T is #[repr(transparent)] over T::Base,
// so Py<T> and Py<T::Base> have the same layout.
unsafe { &*(self as *const Self as *const Py<T::Base>) }
Expand All @@ -2912,9 +2909,9 @@ impl<T: crate::class::PySubclass> Py<T> {
where
T: StaticType,
{
debug_assert!(T::static_type().is_subtype(U::static_type()));
// SAFETY: T is a subtype of U, so Py<T> can be viewed as Py<U>.
unsafe { &*(self as *const Self as *const Py<U>) }
self.as_object()
.downcast_ref::<U>()
.expect("invalid native upcast")
}
}

Expand Down Expand Up @@ -3248,6 +3245,104 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy {
mod tests {
use super::*;

#[test]
fn native_downcasts_validate_the_allocation() {
use crate::{
builtins::builtin_func::{PyNativeFunction, PyNativeMethod},
class::PyClassImpl,
function::{ItemDoc, PyMethodDef, PyMethodFlags},
};

#[pyclass(module = false, name = "AllocationBase")]
#[derive(Debug, PyPayload)]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
struct AllocationBase {
value: usize,
}

#[pyclass(flags(BASETYPE))]
impl AllocationBase {}

#[pyclass(module = false, name = "AllocationDerived", base = AllocationBase)]
#[derive(Debug)]
struct AllocationDerived {
base: AllocationBase,
value: usize,
}

#[pyclass]
impl AllocationDerived {}

crate::Interpreter::without_stdlib(Default::default()).enter(|vm| {
let _ = AllocationBase::make_static_type();
let derived = AllocationDerived::make_static_type();
// A class reference does not enlarge the allocated Rust payload.
let base = PyRef::new_ref(AllocationBase { value: 5 }, derived.clone(), None);
assert!(!base.as_object().downcastable::<AllocationDerived>());
assert!(
base.as_object()
.downcast_ref_if_exact::<AllocationDerived>(vm)
.is_none()
);
assert!(
base.as_object()
.to_owned()
.downcast_exact::<AllocationDerived>(vm)
.is_err()
);
let actual = PyRef::new_ref(
AllocationDerived {
base: AllocationBase { value: 5 },
value: 17,
},
derived,
None,
);
assert_eq!(
actual
.as_object()
.downcast_ref::<AllocationDerived>()
.unwrap()
.value,
17
);
assert_eq!(
actual
.as_object()
.downcast_ref::<AllocationBase>()
.unwrap()
.value,
5
);

const METHOD: PyMethodDef = PyMethodDef::new_const(
"identity",
|value: PyObjectRef| value,
PyMethodFlags::METHOD,
ItemDoc::NONE,
);
let function = METHOD.build_function(&vm.ctx);
// CFunction and CMethod share a Python class, but not a Rust layout.
assert!(!function.as_object().downcastable::<PyNativeMethod>());
assert!(
function
.as_object()
.downcast_ref_if_exact::<PyNativeMethod>(vm)
.is_none()
);
assert!(
function
.as_object()
.to_owned()
.downcast_exact::<PyNativeMethod>(vm)
.is_err()
);
let method =
METHOD.build_bound_method(&vm.ctx, vm.ctx.none(), vm.ctx.types.object_type);
assert!(method.as_object().downcastable::<PyNativeMethod>());
assert!(method.as_object().downcastable::<PyNativeFunction>());
});
}

#[test]
fn native_type_basicsize_includes_payload_padding() {
use crate::class::PyClassDef;
Expand Down
Loading
Loading