Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -671,9 +671,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 @@ -905,9 +917,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
4 changes: 4 additions & 0 deletions crates/vm/src/builtins/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2149,6 +2149,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 @@ -3802,7 +3805,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
188 changes: 162 additions & 26 deletions crates/vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
vm::VirtualMachine,
};
use crate::{
class::StaticType,
class::{PyClassDef, StaticType},
object::traverse::{MaybeTraverse, Traverse, TraverseFn},
};

Expand Down Expand Up @@ -1633,13 +1633,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 @@ -1744,7 +1739,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 @@ -1901,7 +1902,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 @@ -2917,15 +2919,13 @@ where
}
#[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() },
pub fn upcast<U: PyPayload + PyClassDef>(self) -> PyRef<U> {
self.upcast_ref::<U>();
let ptr = self.ptr.cast();
core::mem::forget(self);
PyRef {
ptr,
marker: PhantomData,
}
}
}
Expand All @@ -2940,14 +2940,18 @@ impl<T: crate::class::PySubclass> Py<T> {
unsafe { &*(self as *const Self as *const Py<T::Base>) }
}

/// Converts `&Py<T>` to `&Py<U>` where U is an ancestor type.
/// Converts `&Py<T>` to a compatible native prefix `&Py<U>`.
///
/// The allocation must support U's layout, independently of Python MRO.
#[inline]
pub fn upcast_ref<U: PyPayload + StaticType>(&self) -> &Py<U>
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>.
pub fn upcast_ref<U: PyPayload + PyClassDef>(&self) -> &Py<U> {
let obj = self.as_object();
assert!(
obj.typeid() == U::PAYLOAD_TYPE_ID && obj.supports_native_layout(U::NATIVE_LAYOUT_ID),
"invalid native upcast"
);
// SAFETY: the allocation supports U's native prefix, including its
// payload offset and alignment. Python ancestry is not required.
unsafe { &*(self as *const Self as *const Py<U>) }
}
}
Expand Down Expand Up @@ -3410,6 +3414,138 @@ mod tests {
}
}

#[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)]
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 have distinct Python classes and Rust layouts.
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_upcasts_ignore_python_mro() {
use crate::exceptions::types::{PyException, PyUnicodeError};
use core::panic::AssertUnwindSafe;
use std::panic::catch_unwind;

crate::Interpreter::without_stdlib(Default::default()).enter(|vm| {
let class = vm.ctx.new_class(
None,
"HiddenOSErrorBase",
vm.ctx.exceptions.os_error.to_owned(),
Default::default(),
);
let marker: PyObjectRef = vm.ctx.new_int(42).into();
let error = vm.new_os_subtype_error(class.clone(), None, marker.clone());
// A custom MRO can omit native bases without changing the allocation.
*class.mro.write() = vec![class.clone(), vm.ctx.types.object_type.to_owned()];
assert!(!error.as_object().downcastable::<PyException>());

let base = error.upcast_ref::<PyException>();
assert!(base.as_object().is(error.as_object()));
assert!(base.to_base().get_arg(0).unwrap().is(&marker));
let base = error.clone().upcast::<PyException>();
assert!(base.as_object().is(error.as_object()));
assert!(base.into_base().get_arg(0).unwrap().is(&marker));

// Both exception payloads share PAYLOAD_TYPE_ID, but not their layout.
assert!(
catch_unwind(AssertUnwindSafe(|| error.upcast_ref::<PyUnicodeError>())).is_err()
);
assert!(catch_unwind(AssertUnwindSafe(|| error.upcast::<PyUnicodeError>())).is_err());
});
}

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