Skip to content
Merged
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
1 change: 1 addition & 0 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -1526,6 +1526,7 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
const IS_SUBCLASS: bool = false;
const IS_MAPPING: bool = false;
const IS_SEQUENCE: bool = false;
type Layout = <Self::BaseNativeType as pyo3::impl_::pyclass::PyClassBaseType>::Layout<Self>;
type BaseType = PyAny;
type ThreadChecker = pyo3::impl_::pyclass::SendablePyClass<MyClass>;
type PyClassMutability = <<pyo3::PyAny as pyo3::impl_::pyclass::PyClassBaseType>::PyClassMutability as pyo3::impl_::pycell::PyClassMutability>::MutableChild;
Expand Down
9 changes: 5 additions & 4 deletions pyo3-macros-backend/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2466,18 +2466,17 @@ impl<'a> PyClassImplsBuilder<'a> {

let dict_offset = if self.attr.options.dict.is_some() {
quote! {
fn dict_offset() -> ::std::option::Option<#pyo3_path::ffi::Py_ssize_t> {
fn dict_offset() -> ::std::option::Option<#pyo3_path::impl_::pyclass::PyObjectOffset> {
::std::option::Option::Some(#pyo3_path::impl_::pyclass::dict_offset::<Self>())
}
}
} else {
TokenStream::new()
};

// insert space for weak ref
let weaklist_offset = if self.attr.options.weakref.is_some() {
quote! {
fn weaklist_offset() -> ::std::option::Option<#pyo3_path::ffi::Py_ssize_t> {
fn weaklist_offset() -> ::std::option::Option<#pyo3_path::impl_::pyclass::PyObjectOffset> {
::std::option::Option::Some(#pyo3_path::impl_::pyclass::weaklist_offset::<Self>())
}
}
Expand Down Expand Up @@ -2562,10 +2561,11 @@ impl<'a> PyClassImplsBuilder<'a> {
let pyclass_base_type_impl = attr.options.subclass.map(|subclass| {
quote_spanned! { subclass.span() =>
impl #pyo3_path::impl_::pyclass::PyClassBaseType for #cls {
type LayoutAsBase = #pyo3_path::impl_::pycell::PyClassObject<Self>;
type LayoutAsBase = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::Layout;
type BaseNativeType = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::BaseNativeType;
type Initializer = #pyo3_path::pyclass_init::PyClassInitializer<Self>;
type PyClassMutability = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::PyClassMutability;
type Layout<T: #pyo3_path::impl_::pyclass::PyClassImpl> = <Self::BaseNativeType as #pyo3_path::impl_::pyclass::PyClassBaseType>::Layout<T>;
}
}
});
Expand Down Expand Up @@ -2636,6 +2636,7 @@ impl<'a> PyClassImplsBuilder<'a> {
const IS_SEQUENCE: bool = #is_sequence;
const IS_IMMUTABLE_TYPE: bool = #is_immutable_type;

type Layout = <Self::BaseNativeType as #pyo3_path::impl_::pyclass::PyClassBaseType>::Layout<Self>;
type BaseType = #base;
type ThreadChecker = #thread_checker;
#inventory
Expand Down
3 changes: 2 additions & 1 deletion src/impl_/pycell.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Externally-accessible implementation of pycell
pub use crate::pycell::impl_::{
GetBorrowChecker, PyClassMutability, PyClassObject, PyClassObjectBase, PyClassObjectLayout,
GetBorrowChecker, PyClassMutability, PyClassObjectBase, PyClassObjectBaseLayout,
PyStaticClassObject, PyVariableClassObject, PyVariableClassObjectBase,
};
122 changes: 87 additions & 35 deletions src/impl_/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ use crate::{
ffi_ptr_ext::FfiPtrExt,
impl_::{
freelist::PyObjectFreeList,
pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectLayout},
pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectBaseLayout},
pyclass_init::PyObjectInit,
pymethods::{PyGetterDef, PyMethodDefType},
},
pycell::PyBorrowError,
pycell::{impl_::PyClassObjectLayout, PyBorrowError},
types::{any::PyAnyMethods, PyBool},
Borrowed, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyClass, PyClassGuard, PyErr, PyResult,
PyTypeCheck, PyTypeInfo, Python,
};
use std::{
ffi::CStr,
marker::PhantomData,
mem::offset_of,
os::raw::{c_int, c_void},
ptr::{self, NonNull},
sync::Mutex,
Expand All @@ -35,14 +34,14 @@ pub use probes::*;

/// Gets the offset of the dictionary from the start of the object in bytes.
#[inline]
pub fn dict_offset<T: PyClass>() -> ffi::Py_ssize_t {
PyClassObject::<T>::dict_offset()
pub const fn dict_offset<T: PyClass>() -> PyObjectOffset {
<T as PyClassImpl>::Layout::DICT_OFFSET
}

/// Gets the offset of the weakref list from the start of the object in bytes.
#[inline]
pub fn weaklist_offset<T: PyClass>() -> ffi::Py_ssize_t {
PyClassObject::<T>::weaklist_offset()
pub const fn weaklist_offset<T: PyClass>() -> PyObjectOffset {
<T as PyClassImpl>::Layout::WEAKLIST_OFFSET
}

mod sealed {
Expand Down Expand Up @@ -178,6 +177,9 @@ pub trait PyClassImpl: Sized + 'static {
/// #[pyclass(immutable_type)]
const IS_IMMUTABLE_TYPE: bool = false;

/// Description of how this class is laid out in memory
type Layout: PyClassObjectLayout<Self>;

/// Base class
type BaseType: PyTypeInfo + PyClassBaseType;

Expand Down Expand Up @@ -219,13 +221,17 @@ pub trait PyClassImpl: Sized + 'static {

fn items_iter() -> PyClassItemsIter;

/// Used to provide the __dictoffset__ slot
/// (equivalent to [tp_dictoffset](https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_dictoffset))
#[inline]
fn dict_offset() -> Option<ffi::Py_ssize_t> {
fn dict_offset() -> Option<PyObjectOffset> {
None
}

/// Used to provide the __weaklistoffset__ slot
/// (equivalent to [tp_weaklistoffset](https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_weaklistoffset)
#[inline]
fn weaklist_offset() -> Option<ffi::Py_ssize_t> {
fn weaklist_offset() -> Option<PyObjectOffset> {
None
}

Expand Down Expand Up @@ -886,8 +892,6 @@ macro_rules! generate_pyclass_richcompare_slot {
}
pub use generate_pyclass_richcompare_slot;

use super::pycell::PyClassObject;

/// Implements a freelist.
///
/// Do not implement this trait manually. Instead, use `#[pyclass(freelist = N)]`
Expand Down Expand Up @@ -1110,15 +1114,17 @@ impl<T> PyClassThreadChecker<T> for ThreadCheckerImpl {
)
)]
pub trait PyClassBaseType: Sized {
type LayoutAsBase: PyClassObjectLayout<Self>;
type LayoutAsBase: PyClassObjectBaseLayout<Self>;
type BaseNativeType;
type Initializer: PyObjectInit<Self>;
type PyClassMutability: PyClassMutability;
/// The type of object layout to use for ancestors or descendants of this type.
type Layout<T: PyClassImpl>;
}

/// Implementation of tp_dealloc for pyclasses without gc
pub(crate) unsafe extern "C" fn tp_dealloc<T: PyClass>(obj: *mut ffi::PyObject) {
unsafe { crate::impl_::trampoline::dealloc(obj, PyClassObject::<T>::tp_dealloc) }
unsafe { crate::impl_::trampoline::dealloc(obj, <T as PyClassImpl>::Layout::tp_dealloc) }
}

/// Implementation of tp_dealloc for pyclasses with gc
Expand All @@ -1127,7 +1133,7 @@ pub(crate) unsafe extern "C" fn tp_dealloc_with_gc<T: PyClass>(obj: *mut ffi::Py
unsafe {
ffi::PyObject_GC_UnTrack(obj.cast());
}
unsafe { crate::impl_::trampoline::dealloc(obj, PyClassObject::<T>::tp_dealloc) }
unsafe { crate::impl_::trampoline::dealloc(obj, <T as PyClassImpl>::Layout::tp_dealloc) }
}

pub(crate) unsafe extern "C" fn get_sequence_item_from_mapping(
Expand Down Expand Up @@ -1163,6 +1169,34 @@ pub(crate) unsafe extern "C" fn assign_sequence_item_from_mapping(
}
}

/// Offset of a field within a PyObject in bytes.
#[derive(Debug, Clone, Copy)]
pub enum PyObjectOffset {
/// An offset relative to the start of the object
Absolute(ffi::Py_ssize_t),
/// An offset relative to the start of the subclass-specific data.
/// Only allowed when basicsize is negative (which is only allowed for python >=3.12).
/// <https://docs.python.org/3.12/c-api/structures.html#c.Py_RELATIVE_OFFSET>
#[cfg(Py_3_12)]
Relative(ffi::Py_ssize_t),
}

impl std::ops::Add<usize> for PyObjectOffset {
type Output = PyObjectOffset;

fn add(self, rhs: usize) -> Self::Output {
// Py_ssize_t may not be equal to isize on all platforms
#[allow(clippy::useless_conversion)]
let rhs: ffi::Py_ssize_t = rhs.try_into().expect("offset should fit in Py_ssize_t");

match self {
PyObjectOffset::Absolute(offset) => PyObjectOffset::Absolute(offset + rhs),
#[cfg(Py_3_12)]
PyObjectOffset::Relative(offset) => PyObjectOffset::Relative(offset + rhs),
}
}
}

/// Type which uses specialization on impl blocks to determine how to read a field from a Rust pyclass
/// as part of a `#[pyo3(get)]` annotation.
pub struct PyClassGetterGenerator<
Expand Down Expand Up @@ -1226,11 +1260,19 @@ impl<
pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType {
use crate::pyclass::boolean_struct::private::Boolean;
if ClassT::Frozen::VALUE {
let (offset, flags) = match <ClassT as PyClassImpl>::Layout::CONTENTS_OFFSET {
PyObjectOffset::Absolute(offset) => (offset, ffi::Py_READONLY),
#[cfg(Py_3_12)]
PyObjectOffset::Relative(offset) => {
(offset, ffi::Py_READONLY | ffi::Py_RELATIVE_OFFSET)
}
};

PyMethodDefType::StructMember(ffi::PyMemberDef {
name: name.as_ptr(),
type_code: ffi::Py_T_OBJECT_EX,
offset: (offset_of!(PyClassObject<ClassT>, contents) + OFFSET) as ffi::Py_ssize_t,
flags: ffi::Py_READONLY,
offset: offset + OFFSET as ffi::Py_ssize_t,
flags,
doc: doc.as_ptr(),
})
} else {
Expand Down Expand Up @@ -1315,7 +1357,7 @@ where
/// - value of type `FieldT` must exist at the given offset within obj
unsafe fn inner<FieldT>(
py: Python<'_>,
obj: *mut ffi::PyObject,
obj: *const (),
offset: usize,
) -> PyResult<*mut ffi::PyObject>
where
Expand All @@ -1328,14 +1370,12 @@ where

// SAFETY: `obj` is a valid pointer to `ClassT`
let _holder = unsafe { ensure_no_mutable_alias::<ClassT>(py, &obj)? };
let class_ptr = obj.cast::<<ClassT as PyClassImpl>::Layout>();
let class_obj = unsafe { &*class_ptr };
let contents_ptr = ptr::from_ref(class_obj.contents());

// SAFETY: _holder prevents mutable aliasing, caller upholds other safety invariants
unsafe {
inner::<FieldT>(
py,
obj,
offset_of!(PyClassObject<ClassT>, contents) + OFFSET,
)
}
unsafe { inner::<FieldT>(py, contents_ptr.cast(), OFFSET) }
}

/// Gets a field value from a pyclass and produces a python value using `IntoPyObject` for `FieldT`,
Expand All @@ -1359,7 +1399,7 @@ where
/// - value of type `FieldT` must exist at the given offset within obj
unsafe fn inner<FieldT>(
py: Python<'_>,
obj: *mut ffi::PyObject,
obj: *const (),
offset: usize,
) -> PyResult<*mut ffi::PyObject>
where
Expand All @@ -1372,14 +1412,12 @@ where

// SAFETY: `obj` is a valid pointer to `ClassT`
let _holder = unsafe { ensure_no_mutable_alias::<ClassT>(py, &obj)? };
let class_ptr = obj.cast::<<ClassT as PyClassImpl>::Layout>();
let class_obj = unsafe { &*class_ptr };
let contents_ptr = ptr::from_ref(class_obj.contents());

// SAFETY: _holder prevents mutable aliasing, caller upholds other safety invariants
unsafe {
inner::<FieldT>(
py,
obj,
offset_of!(PyClassObject<ClassT>, contents) + OFFSET,
)
}
unsafe { inner::<FieldT>(py, contents_ptr.cast(), OFFSET) }
}

pub struct ConvertField<
Expand Down Expand Up @@ -1412,7 +1450,10 @@ pub trait ExtractPyClassWithClone {}
#[cfg(test)]
#[cfg(feature = "macros")]
mod tests {
use crate::pycell::impl_::PyClassObjectContents;

use super::*;
use std::mem::offset_of;

#[test]
fn get_py_for_frozen_class() {
Expand All @@ -1437,10 +1478,15 @@ mod tests {
Some(PyMethodDefType::StructMember(member)) => {
assert_eq!(unsafe { CStr::from_ptr(member.name) }, c"value");
assert_eq!(member.type_code, ffi::Py_T_OBJECT_EX);
#[repr(C)]
struct ExpectedLayout {
ob_base: ffi::PyObject,
contents: PyClassObjectContents<FrozenClass>,
}
Comment on lines +1481 to +1485
Copy link
Member

Choose a reason for hiding this comment

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

Should this be using PyStaticClassObject?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is redefined here, because the fields of PyStaticClassObject are intentionally private, so we can't compute the offset here without loosening that.

assert_eq!(
member.offset,
(offset_of!(PyClassObject<FrozenClass>, contents)
+ offset_of!(FrozenClass, value)) as ffi::Py_ssize_t
(offset_of!(ExpectedLayout, contents) + offset_of!(FrozenClass, value))
as ffi::Py_ssize_t
);
assert_eq!(member.flags, ffi::Py_READONLY);
}
Expand Down Expand Up @@ -1551,9 +1597,15 @@ mod tests {
// SAFETY: def.doc originated from a CStr
assert_eq!(unsafe { CStr::from_ptr(def.doc) }, c"My field doc");
assert_eq!(def.type_code, ffi::Py_T_OBJECT_EX);
#[allow(irrefutable_let_patterns)]
let PyObjectOffset::Absolute(contents_offset) =
<MyClass as PyClassImpl>::Layout::CONTENTS_OFFSET
else {
panic!()
};
assert_eq!(
def.offset,
(offset_of!(PyClassObject<MyClass>, contents) + FIELD_OFFSET) as ffi::Py_ssize_t
contents_offset + FIELD_OFFSET as ffi::Py_ssize_t
);
assert_eq!(def.flags, ffi::Py_READONLY);
}
Expand Down
15 changes: 8 additions & 7 deletions src/impl_/pymethods.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::exceptions::PyStopAsyncIteration;
use crate::impl_::callback::IntoPyCallbackOutput;
use crate::impl_::panic::PanicTrap;
use crate::impl_::pycell::{PyClassObject, PyClassObjectLayout};
use crate::impl_::pycell::PyClassObjectBaseLayout;
use crate::internal::get_slot::{get_slot, TP_BASE, TP_CLEAR, TP_TRAVERSE};
use crate::internal::state::ForbidAttaching;
use crate::pycell::impl_::PyClassBorrowChecker as _;
use crate::pycell::impl_::{PyClassBorrowChecker as _, PyClassObjectLayout};
use crate::pycell::{PyBorrowError, PyBorrowMutError};
use crate::pyclass::boolean_struct::False;
use crate::types::PyType;
Expand All @@ -19,6 +19,7 @@ use std::marker::PhantomData;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr::{null_mut, NonNull};

use super::pyclass::PyClassImpl;
use super::trampoline;
use crate::internal_tricks::{clear_eq, traverse_eq};

Expand Down Expand Up @@ -306,25 +307,25 @@ where

// SAFETY: `slf` is a valid Python object pointer to a class object of type T, and
// traversal is running so no mutations can occur.
let class_object: &PyClassObject<T> = unsafe { &*slf.cast() };
let class_object: &<T as PyClassImpl>::Layout = unsafe { &*slf.cast() };

let retval =
// `#[pyclass(unsendable)]` types can only be deallocated by their own thread, so
// do not traverse them if not on their owning thread :(
if class_object.check_threadsafe().is_ok()
// ... and we cannot traverse a type which might be being mutated by a Rust thread
&& class_object.borrow_checker().try_borrow().is_ok() {
struct TraverseGuard<'a, T: PyClass>(&'a PyClassObject<T>);
impl<T: PyClass> Drop for TraverseGuard<'_, T> {
struct TraverseGuard<'a, T: PyClassImpl>(&'a T::Layout);
impl<T: PyClassImpl> Drop for TraverseGuard<'_, T> {
fn drop(&mut self) {
self.0.borrow_checker().release_borrow()
}
}

// `.try_borrow()` above created a borrow, we need to release it when we're done
// traversing the object. This allows us to read `instance` safely.
let _guard = TraverseGuard(class_object);
let instance = unsafe {&*class_object.contents.value.get()};
let _guard = TraverseGuard::<T>(class_object);
let instance = unsafe {&*class_object.contents().value.get()};

let visit = PyVisit { visit, arg, _guard: PhantomData };

Expand Down
Loading
Loading