#![allow(missing_docs)] //! Crate-private implementation of PyClassObject use core::cell::UnsafeCell; use core::marker::PhantomData; use core::mem::{offset_of, ManuallyDrop, MaybeUninit}; use core::sync::atomic::{AtomicUsize, Ordering}; use crate::impl_::pyclass::{ PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, PyObjectOffset, }; use crate::internal::get_slot::{TP_DEALLOC, TP_FREE}; #[cfg(RustPython)] use crate::sync::PyOnceLock; use crate::type_object::{PyLayout, PySizedLayout, PyTypeInfo}; use crate::types::PyType; use crate::{ffi, PyClass, Python}; use crate::types::PyTypeMethods; use super::{PyBorrowError, PyBorrowMutError}; pub trait PyClassMutability { // The storage for this inheritance layer. Only the first mutable class in // an inheritance hierarchy needs to store the borrow flag. type Storage: PyClassBorrowChecker; // The borrow flag needed to implement this class' mutability. Empty until // the first mutable class, at which point it is BorrowChecker and will be // for all subclasses. type Checker: PyClassBorrowChecker; type ImmutableChild: PyClassMutability; type MutableChild: PyClassMutability; } pub struct ImmutableClass(()); pub struct MutableClass(()); pub struct ExtendsMutableAncestor(PhantomData); impl PyClassMutability for ImmutableClass { type Storage = EmptySlot; type Checker = EmptySlot; type ImmutableChild = ImmutableClass; type MutableChild = MutableClass; } impl PyClassMutability for MutableClass { type Storage = BorrowChecker; type Checker = BorrowChecker; type ImmutableChild = ExtendsMutableAncestor; type MutableChild = ExtendsMutableAncestor; } impl PyClassMutability for ExtendsMutableAncestor { type Storage = EmptySlot; type Checker = BorrowChecker; type ImmutableChild = ExtendsMutableAncestor; type MutableChild = ExtendsMutableAncestor; } #[derive(Debug)] struct BorrowFlag(AtomicUsize); impl BorrowFlag { pub(crate) const UNUSED: usize = 0; const HAS_MUTABLE_BORROW: usize = usize::MAX; fn increment(&self) -> Result<(), PyBorrowError> { // relaxed is OK because we will read the value again in the compare_exchange let mut value = self.0.load(Ordering::Relaxed); loop { if value == BorrowFlag::HAS_MUTABLE_BORROW { return Err(PyBorrowError { _private: () }); } match self.0.compare_exchange( // only increment if the value hasn't changed since the // last atomic load value, value + 1, // reading the value is happens-after a previous write // writing the new value is happens-after the previous read Ordering::AcqRel, // relaxed is OK here because we're going to try to read again Ordering::Relaxed, ) { Ok(..) => { break Ok(()); } Err(changed_value) => { // value changed under us, need to try again value = changed_value; } } } } fn decrement(&self) { // relaxed load is OK but decrements must happen-before the next read self.0.fetch_sub(1, Ordering::Release); } } pub struct EmptySlot(()); pub struct BorrowChecker(BorrowFlag); pub trait PyClassBorrowChecker { /// Initial value for self fn new() -> Self where Self: Sized; /// Increments immutable borrow count, if possible fn try_borrow(&self) -> Result<(), PyBorrowError>; /// Decrements immutable borrow count fn release_borrow(&self); /// Increments mutable borrow count, if possible fn try_borrow_mut(&self) -> Result<(), PyBorrowMutError>; /// Decremements mutable borrow count fn release_borrow_mut(&self); } impl PyClassBorrowChecker for EmptySlot { #[inline] fn new() -> Self { EmptySlot(()) } #[inline] fn try_borrow(&self) -> Result<(), PyBorrowError> { Ok(()) } #[inline] fn release_borrow(&self) {} #[inline] fn try_borrow_mut(&self) -> Result<(), PyBorrowMutError> { unreachable!() } #[inline] fn release_borrow_mut(&self) { unreachable!() } } impl PyClassBorrowChecker for BorrowChecker { #[inline] fn new() -> Self { Self(BorrowFlag(AtomicUsize::new(BorrowFlag::UNUSED))) } fn try_borrow(&self) -> Result<(), PyBorrowError> { self.0.increment() } fn release_borrow(&self) { self.0.decrement(); } fn try_borrow_mut(&self) -> Result<(), PyBorrowMutError> { let flag = &self.0; match flag.0.compare_exchange( // only allowed to transition to mutable borrow if the reference is // currently unused BorrowFlag::UNUSED, BorrowFlag::HAS_MUTABLE_BORROW, // On success, reading the flag and updating its state are an atomic // operation Ordering::AcqRel, // It doesn't matter precisely when the failure gets turned // into an error Ordering::Relaxed, ) { Ok(..) => Ok(()), Err(..) => Err(PyBorrowMutError { _private: () }), } } fn release_borrow_mut(&self) { self.0 .0.store(BorrowFlag::UNUSED, Ordering::Release) } } pub trait GetBorrowChecker { fn borrow_checker( class_object: &T::Layout, ) -> &::Checker; } impl> GetBorrowChecker for MutableClass { fn borrow_checker(class_object: &T::Layout) -> &BorrowChecker { &class_object.contents().borrow_checker } } impl> GetBorrowChecker for ImmutableClass { fn borrow_checker(class_object: &T::Layout) -> &EmptySlot { &class_object.contents().borrow_checker } } impl, M: PyClassMutability> GetBorrowChecker for ExtendsMutableAncestor where T::BaseType: PyClassImpl + PyClassBaseType::Layout>, ::PyClassMutability: PyClassMutability, { fn borrow_checker(class_object: &T::Layout) -> &BorrowChecker { <::PyClassMutability as GetBorrowChecker>::borrow_checker(class_object.ob_base()) } } /// Base layout of PyClassObject with a known sized base type. /// Corresponds to [PyObject](https://docs.python.org/3/c-api/structures.html#c.PyObject) from the C API. #[doc(hidden)] #[repr(C)] pub struct PyClassObjectBase { ob_base: T, } unsafe impl PyLayout for PyClassObjectBase where U: PySizedLayout {} impl PyClassObjectBaseLayout for PyClassObjectBase where U: PySizedLayout, T: PyTypeInfo, { fn ensure_threadsafe(&self) {} fn check_threadsafe(&self) -> Result<(), PyBorrowError> { Ok(()) } unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { unsafe { tp_dealloc(slf, &T::type_object(py)) }; } } /// Base layout of PyClassObject with an unknown sized base type. /// Corresponds to [PyVarObject](https://docs.python.org/3/c-api/structures.html#c.PyVarObject) from the C API. #[doc(hidden)] #[repr(C)] pub struct PyVariableClassObjectBase { ob_base: ffi::PyVarObject, } unsafe impl PyLayout for PyVariableClassObjectBase {} impl PyClassObjectBaseLayout for PyVariableClassObjectBase { fn ensure_threadsafe(&self) {} fn check_threadsafe(&self) -> Result<(), PyBorrowError> { Ok(()) } unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { unsafe { tp_dealloc(slf, &T::type_object(py)) }; } } /// Implementation of tp_dealloc. /// # Safety /// - `slf` must be a valid pointer to an instance of the type at `type_obj` or a subclass. /// - `slf` must not be used after this call (as it will be freed). unsafe fn tp_dealloc(slf: *mut ffi::PyObject, type_obj: &crate::Bound<'_, PyType>) { let py = type_obj.py(); unsafe { // FIXME: there is potentially subtle issues here if the base is overwritten // at runtime? To be investigated. let type_ptr = type_obj.as_type_ptr(); let actual_type_ptr = ffi::Py_TYPE(slf); // For heap types, instances must decref the type object when they // are deallocated, so we create a bound from a borrowed pointer as // as if it was an owned pointer. In this way, when the bound is dropped, // it will decref the type object. debug_assert!(ffi::PyType_HasFeature(actual_type_ptr, ffi::Py_TPFLAGS_HEAPTYPE) != 0); let actual_type = cfg_select! { not(PyPy) => crate::Bound::from_owned_ptr(py, actual_type_ptr as *mut ffi::PyObject) .cast_into_unchecked::(), // See https://github.com/pypy/pypy/issues/5555 - it seems that PyPy does not // support the CPython semantics properly, so we avoid taking ownership of the // type object on PyPy. // // TODO: If the PyPy bug is fixed we should remove this workaround and just create // a `Bound` as above. PyPy => crate::Borrowed::from_ptr(py, actual_type_ptr as *mut ffi::PyObject) .cast_unchecked::(), }; // For `#[pyclass]` types which inherit from PyAny, we can just call tp_free #[cfg(not(RustPython))] let base_object_type_ptr = &raw const ffi::PyBaseObject_Type; #[cfg(RustPython)] let base_object_type_ptr = { static TYPE: PyOnceLock> = PyOnceLock::new(); TYPE.import(py, "builtins", "object").unwrap().as_type_ptr() }; if core::ptr::eq(type_ptr, base_object_type_ptr) { let tp_free = actual_type .get_slot(TP_FREE) .expect("PyBaseObject_Type should have tp_free"); tp_free(slf.cast()); } // More complex native types (e.g. `extends=PyDict`) require calling the base's dealloc. // FIXME: should this be using actual_type.tp_dealloc? else if let Some(dealloc) = type_obj.get_slot(TP_DEALLOC) { // Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which // assumes the exception is currently GC tracked, so we have to re-track // before calling the dealloc so that it can safely call Py_GC_UNTRACK. #[cfg(not(any(Py_3_11, PyPy)))] if ffi::PyType_FastSubclass(type_ptr, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 { ffi::PyObject_GC_Track(slf.cast()); } dealloc(slf); } else { type_obj.get_slot(TP_FREE).expect("type missing tp_free")(slf.cast()); } // Cause the reference to the type to be decrefed for heap types, which // is necessary to avoid a reference leak. #[cfg_attr( PyPy, expect(dropping_copy_types, reason = "see PyPy workaround for decref above") )] drop(actual_type); } } /// functionality common to all PyObjects regardless of the layout #[doc(hidden)] pub trait PyClassObjectBaseLayout: PyLayout { fn ensure_threadsafe(&self); fn check_threadsafe(&self) -> Result<(), PyBorrowError>; /// Implementation of tp_dealloc. /// # Safety /// - slf must be a valid pointer to an instance of a T or a subclass. /// - slf must not be used after this call (as it will be freed). unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject); } /// Functionality required for creating and managing the memory associated with a pyclass annotated struct. #[doc(hidden)] #[diagnostic::on_unimplemented( message = "the class layout is not valid", label = "required for `#[pyclass(extends=...)]`", note = "the python version being built against influences which layouts are valid" )] pub trait PyClassObjectLayout: PyClassObjectBaseLayout { /// Gets the offset of the contents from the start of the struct in bytes. const CONTENTS_OFFSET: PyObjectOffset; /// Used to set `PyType_Spec::basicsize` /// ([docs](https://docs.python.org/3/c-api/type.html#c.PyType_Spec.basicsize)) const BASIC_SIZE: ffi::Py_ssize_t; /// Gets the offset of the dictionary from the start of the struct in bytes. const DICT_OFFSET: PyObjectOffset; /// Gets the offset of the weakref list from the start of the struct in bytes. const WEAKLIST_OFFSET: PyObjectOffset; /// Obtain a pointer to the contents of an uninitialized PyObject of this type. /// /// SAFETY: `obj` must have the layout that the implementation is expecting unsafe fn contents_uninit( obj: *mut ffi::PyObject, ) -> *mut MaybeUninit>; /// Obtain a reference to the structure that contains the pyclass struct and associated metadata. fn contents(&self) -> &PyClassObjectContents; /// Obtain a mutable reference to the structure that contains the pyclass struct and associated metadata. fn contents_mut(&mut self) -> &mut PyClassObjectContents; /// Obtain a pointer to the pyclass struct. fn get_ptr(&self) -> *mut T; /// obtain a reference to the data at the start of the PyObject. fn ob_base(&self) -> &::LayoutAsBase; fn borrow_checker(&self) -> &::Checker; } #[repr(C)] pub struct PyClassObjectContents { pub(crate) value: ManuallyDrop>, pub(crate) borrow_checker: ::Storage, pub(crate) thread_checker: T::ThreadChecker, pub(crate) dict: T::Dict, pub(crate) weakref: T::WeakRef, } impl PyClassObjectContents { pub(crate) fn new(init: T) -> Self { PyClassObjectContents { value: ManuallyDrop::new(UnsafeCell::new(init)), borrow_checker: ::Storage::new(), thread_checker: T::ThreadChecker::new(), dict: T::Dict::INIT, weakref: T::WeakRef::INIT, } } unsafe fn dealloc(&mut self, py: Python<'_>, py_object: *mut ffi::PyObject) { if self.thread_checker.can_drop(py) { unsafe { ManuallyDrop::drop(&mut self.value) }; } self.dict.release_dict(py); unsafe { self.weakref.clear_weakrefs(py_object, py) }; } } /// The layout of a PyClassObject with a known sized base class. #[repr(C)] pub struct PyStaticClassObject { ob_base: ::LayoutAsBase, contents: PyClassObjectContents, } impl> PyClassObjectLayout for PyStaticClassObject { /// Gets the offset of the contents from the start of the struct in bytes. const CONTENTS_OFFSET: PyObjectOffset = { let offset = offset_of!(Self, contents); // Py_ssize_t may not be equal to isize on all platforms assert!(offset <= ffi::Py_ssize_t::MAX as usize); PyObjectOffset::Absolute(offset as ffi::Py_ssize_t) }; const BASIC_SIZE: ffi::Py_ssize_t = { let size = core::mem::size_of::(); assert!(size <= ffi::Py_ssize_t::MAX as usize); size as _ }; const DICT_OFFSET: PyObjectOffset = { let offset = offset_of!(PyStaticClassObject, contents) + offset_of!(PyClassObjectContents, dict); assert!(offset <= ffi::Py_ssize_t::MAX as usize); PyObjectOffset::Absolute(offset as _) }; const WEAKLIST_OFFSET: PyObjectOffset = { let offset = offset_of!(PyStaticClassObject, contents) + offset_of!(PyClassObjectContents, weakref); assert!(offset <= ffi::Py_ssize_t::MAX as usize); PyObjectOffset::Absolute(offset as _) }; unsafe fn contents_uninit( obj: *mut ffi::PyObject, ) -> *mut MaybeUninit> { #[repr(C)] struct PartiallyInitializedClassObject { _ob_base: ::LayoutAsBase, contents: MaybeUninit>, } let obj = obj.cast::>(); unsafe { &raw mut (*obj).contents } } fn contents(&self) -> &PyClassObjectContents { &self.contents } fn contents_mut(&mut self) -> &mut PyClassObjectContents { &mut self.contents } fn get_ptr(&self) -> *mut T { self.contents.value.get() } fn ob_base(&self) -> &::LayoutAsBase { &self.ob_base } fn borrow_checker(&self) -> &::Checker { T::PyClassMutability::borrow_checker(self) } } unsafe impl PyLayout for PyStaticClassObject {} impl PySizedLayout for PyStaticClassObject {} impl> PyClassObjectBaseLayout for PyStaticClassObject where ::LayoutAsBase: PyClassObjectBaseLayout, { fn ensure_threadsafe(&self) { self.contents.thread_checker.ensure(); self.ob_base.ensure_threadsafe(); } fn check_threadsafe(&self) -> Result<(), PyBorrowError> { if !self.contents.thread_checker.check() { return Err(PyBorrowError { _private: () }); } self.ob_base.check_threadsafe() } unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { // Safety: Python only calls tp_dealloc when no references to the object remain. let class_object = unsafe { &mut *(slf.cast::()) }; unsafe { class_object.contents_mut().dealloc(py, slf) }; unsafe { ::LayoutAsBase::tp_dealloc(py, slf) } } } /// A layout for a PyClassObject with an unknown sized base type. /// /// Utilises [PEP-697](https://peps.python.org/pep-0697/) #[doc(hidden)] #[repr(C)] pub struct PyVariableClassObject { ob_base: ::LayoutAsBase, } #[cfg(Py_3_12)] impl> PyVariableClassObject { /// # Safety /// - `obj` must have the layout that the implementation is expecting /// - thread must be attached to the interpreter unsafe fn get_contents_of_obj( obj: *mut ffi::PyObject, ) -> *mut MaybeUninit> { // TODO: it would be nice to eventually avoid coupling to the PyO3 statics here, maybe using // 3.14's PyType_GetBaseByToken, to support PEP 587 / multiple interpreters better // SAFETY: caller guarantees attached to the interpreter let type_obj = T::type_object_raw(unsafe { Python::assume_attached() }); let pointer = unsafe { ffi::PyObject_GetTypeData(obj, type_obj) }; pointer.cast() } fn get_contents_ptr(&self) -> *mut PyClassObjectContents { unsafe { Self::get_contents_of_obj(self as *const PyVariableClassObject as *mut ffi::PyObject) } .cast() } } #[cfg(Py_3_12)] impl> PyClassObjectLayout for PyVariableClassObject { /// Gets the offset of the contents from the start of the struct in bytes. const CONTENTS_OFFSET: PyObjectOffset = PyObjectOffset::Relative(0); const BASIC_SIZE: ffi::Py_ssize_t = { let size = core::mem::size_of::>(); assert!(size <= ffi::Py_ssize_t::MAX as usize); // negative to indicate 'extra' space that cpython will allocate for us -(size as ffi::Py_ssize_t) }; const DICT_OFFSET: PyObjectOffset = { let offset = offset_of!(PyClassObjectContents, dict); assert!(offset <= ffi::Py_ssize_t::MAX as usize); PyObjectOffset::Relative(offset as _) }; const WEAKLIST_OFFSET: PyObjectOffset = { let offset = offset_of!(PyClassObjectContents, weakref); assert!(offset <= ffi::Py_ssize_t::MAX as usize); PyObjectOffset::Relative(offset as _) }; unsafe fn contents_uninit( obj: *mut ffi::PyObject, ) -> *mut MaybeUninit> { unsafe { Self::get_contents_of_obj(obj) } } fn get_ptr(&self) -> *mut T { self.contents().value.get() } fn ob_base(&self) -> &::LayoutAsBase { &self.ob_base } fn contents(&self) -> &PyClassObjectContents { unsafe { self.get_contents_ptr().cast_const().as_ref() } .expect("should be able to cast PyClassObjectContents pointer") } fn contents_mut(&mut self) -> &mut PyClassObjectContents { unsafe { self.get_contents_ptr().as_mut() } .expect("should be able to cast PyClassObjectContents pointer") } fn borrow_checker(&self) -> &::Checker { T::PyClassMutability::borrow_checker(self) } } unsafe impl PyLayout for PyVariableClassObject {} #[cfg(Py_3_12)] impl> PyClassObjectBaseLayout for PyVariableClassObject where ::LayoutAsBase: PyClassObjectBaseLayout, { fn ensure_threadsafe(&self) { self.contents().thread_checker.ensure(); self.ob_base.ensure_threadsafe(); } fn check_threadsafe(&self) -> Result<(), PyBorrowError> { if !self.contents().thread_checker.check() { return Err(PyBorrowError { _private: () }); } self.ob_base.check_threadsafe() } unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { // Safety: Python only calls tp_dealloc when no references to the object remain. let class_object = unsafe { &mut *(slf.cast::()) }; unsafe { class_object.contents_mut().dealloc(py, slf) }; unsafe { ::LayoutAsBase::tp_dealloc(py, slf) } } } #[cfg(test)] #[cfg(feature = "macros")] mod tests { use super::*; use crate::prelude::*; use crate::pyclass::boolean_struct::{False, True}; #[pyclass(crate = "crate", subclass)] struct MutableBase; #[pyclass(crate = "crate", extends = MutableBase, subclass)] struct MutableChildOfMutableBase; #[pyclass(crate = "crate", extends = MutableBase, frozen, subclass)] struct ImmutableChildOfMutableBase; #[pyclass(crate = "crate", extends = MutableChildOfMutableBase)] struct MutableChildOfMutableChildOfMutableBase; #[pyclass(crate = "crate", extends = ImmutableChildOfMutableBase)] struct MutableChildOfImmutableChildOfMutableBase; #[pyclass(crate = "crate", extends = MutableChildOfMutableBase, frozen)] struct ImmutableChildOfMutableChildOfMutableBase; #[pyclass(crate = "crate", extends = ImmutableChildOfMutableBase, frozen)] struct ImmutableChildOfImmutableChildOfMutableBase; #[pyclass(crate = "crate", frozen, subclass)] struct ImmutableBase; #[pyclass(crate = "crate", extends = ImmutableBase, subclass)] struct MutableChildOfImmutableBase; #[pyclass(crate = "crate", extends = ImmutableBase, frozen, subclass)] struct ImmutableChildOfImmutableBase; #[pyclass(crate = "crate", extends = MutableChildOfImmutableBase)] struct MutableChildOfMutableChildOfImmutableBase; #[pyclass(crate = "crate", extends = ImmutableChildOfImmutableBase)] struct MutableChildOfImmutableChildOfImmutableBase; #[pyclass(crate = "crate", extends = MutableChildOfImmutableBase, frozen)] struct ImmutableChildOfMutableChildOfImmutableBase; #[pyclass(crate = "crate", extends = ImmutableChildOfImmutableBase, frozen)] struct ImmutableChildOfImmutableChildOfImmutableBase; #[pyclass(crate = "crate", subclass)] struct BaseWithoutData; #[pyclass(crate = "crate", subclass)] struct BaseWithData(#[allow(unused)] u64); #[pyclass(crate = "crate", extends = BaseWithData)] struct ChildWithData(#[allow(unused)] u64); #[pyclass(crate = "crate", extends = BaseWithData)] struct ChildWithoutData; #[test] fn test_inherited_size() { #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] type ClassObject = PyVariableClassObject; #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] type ClassObject = PyStaticClassObject; let base_without_data_size = ClassObject::::BASIC_SIZE; let base_with_data_size = ClassObject::::BASIC_SIZE; let child_without_data_size = ClassObject::::BASIC_SIZE; let child_with_data_size = ClassObject::::BASIC_SIZE; #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] { assert!(base_without_data_size < 0); // negative indicates variable sized assert!(base_with_data_size < base_without_data_size); assert_eq!(child_without_data_size, 0); assert_eq!( base_with_data_size - base_without_data_size, child_with_data_size ); } #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] { assert!(base_without_data_size > 0); assert!(base_with_data_size > base_without_data_size); assert_eq!(base_with_data_size, child_without_data_size); assert!(base_with_data_size < child_with_data_size); } } fn assert_mutable>() {} fn assert_immutable>() {} fn assert_mutable_with_mutable_ancestor< T: PyClass>, >() { } fn assert_immutable_with_mutable_ancestor< T: PyClass>, >() { } #[test] fn test_inherited_mutability() { // mutable base assert_mutable::(); // children of mutable base have a mutable ancestor assert_mutable_with_mutable_ancestor::(); assert_immutable_with_mutable_ancestor::(); // grandchildren of mutable base have a mutable ancestor assert_mutable_with_mutable_ancestor::(); assert_mutable_with_mutable_ancestor::(); assert_immutable_with_mutable_ancestor::(); assert_immutable_with_mutable_ancestor::(); // immutable base and children assert_immutable::(); assert_immutable::(); assert_immutable::(); // mutable children of immutable at any level are simply mutable assert_mutable::(); assert_mutable::(); // children of the mutable child display this property assert_mutable_with_mutable_ancestor::(); assert_immutable_with_mutable_ancestor::(); } #[test] fn test_mutable_borrow_prevents_further_borrows() { Python::attach(|py| { let mmm = Py::new( py, PyClassInitializer::from(MutableBase) .add_subclass(MutableChildOfMutableBase) .add_subclass(MutableChildOfMutableChildOfMutableBase), ) .unwrap(); let mmm_bound: &Bound<'_, MutableChildOfMutableChildOfMutableBase> = mmm.bind(py); let mmm_refmut = mmm_bound.borrow_mut(); // Cannot take any other mutable or immutable borrows whilst the object is borrowed mutably assert!(mmm_bound .extract::>() .is_err()); assert!(mmm_bound .extract::>() .is_err()); assert!(mmm_bound.extract::>().is_err()); assert!(mmm_bound .extract::>() .is_err()); assert!(mmm_bound .extract::>() .is_err()); assert!(mmm_bound.extract::>().is_err()); // With the borrow dropped, all other borrow attempts will succeed drop(mmm_refmut); assert!(mmm_bound .extract::>() .is_ok()); assert!(mmm_bound .extract::>() .is_ok()); assert!(mmm_bound.extract::>().is_ok()); assert!(mmm_bound .extract::>() .is_ok()); assert!(mmm_bound .extract::>() .is_ok()); assert!(mmm_bound.extract::>().is_ok()); }) } #[test] fn test_immutable_borrows_prevent_mutable_borrows() { Python::attach(|py| { let mmm = Py::new( py, PyClassInitializer::from(MutableBase) .add_subclass(MutableChildOfMutableBase) .add_subclass(MutableChildOfMutableChildOfMutableBase), ) .unwrap(); let mmm_bound: &Bound<'_, MutableChildOfMutableChildOfMutableBase> = mmm.bind(py); let mmm_refmut = mmm_bound.borrow(); // Further immutable borrows are ok assert!(mmm_bound .extract::>() .is_ok()); assert!(mmm_bound .extract::>() .is_ok()); assert!(mmm_bound.extract::>().is_ok()); // Further mutable borrows are not ok assert!(mmm_bound .extract::>() .is_err()); assert!(mmm_bound .extract::>() .is_err()); assert!(mmm_bound.extract::>().is_err()); // With the borrow dropped, all mutable borrow attempts will succeed drop(mmm_refmut); assert!(mmm_bound .extract::>() .is_ok()); assert!(mmm_bound .extract::>() .is_ok()); assert!(mmm_bound.extract::>().is_ok()); }) } #[test] #[cfg(not(target_arch = "wasm32"))] fn test_thread_safety() { #[crate::pyclass(crate = "crate")] struct MyClass { x: u64, } Python::attach(|py| { let inst = Py::new(py, MyClass { x: 0 }).unwrap(); let total_modifications = py.detach(|| { std::thread::scope(|s| { // Spawn a bunch of threads all racing to write to // the same instance of `MyClass`. let threads = (0..10) .map(|_| { s.spawn(|| { Python::attach(|py| { // Each thread records its own view of how many writes it made let mut local_modifications = 0; for _ in 0..100 { if let Ok(mut i) = inst.try_borrow_mut(py) { i.x += 1; local_modifications += 1; } } local_modifications }) }) }) .collect::>(); // Sum up the total number of writes made by all threads threads.into_iter().map(|t| t.join().unwrap()).sum::() }) }); // If the implementation is free of data races, the total number of writes // should match the final value of `x`. assert_eq!(total_modifications, inst.borrow(py).x); }); } #[test] #[cfg(not(target_arch = "wasm32"))] fn test_thread_safety_2() { struct SyncUnsafeCell(UnsafeCell); unsafe impl Sync for SyncUnsafeCell {} impl SyncUnsafeCell { fn get(&self) -> *mut T { self.0.get() } } let data = SyncUnsafeCell(UnsafeCell::new(0)); let data2 = SyncUnsafeCell(UnsafeCell::new(0)); let borrow_checker = BorrowChecker(BorrowFlag(AtomicUsize::new(BorrowFlag::UNUSED))); std::thread::scope(|s| { s.spawn(|| { for _ in 0..1_000_000 { if borrow_checker.try_borrow_mut().is_ok() { // thread 1 writes to both values during the mutable borrow unsafe { *data.get() += 1 }; unsafe { *data2.get() += 1 }; borrow_checker.release_borrow_mut(); } } }); s.spawn(|| { for _ in 0..1_000_000 { if borrow_checker.try_borrow().is_ok() { // if the borrow checker is working correctly, it should be impossible // for thread 2 to observe a difference in the two values assert_eq!(unsafe { *data.get() }, unsafe { *data2.get() }); borrow_checker.release_borrow(); } } }); }); } }