Unify PyObjectRef ownership with extensible PyRef views (#9080)

Represent PyObjectRef through PyRef<PyObject> while preserving typed
ownership, payload projection, and drop behavior. Use the sealed target
selector for borrowed set/frozenset union views and typed tuple elements.

Keep tuple storage erased behind invariant typed slice views, with checked
conversions and reference-count ownership tests. Preserve exact tuple
identity for normalized nonempty full slices. Integrate the ownership
contract into the public object module documentation and retain focused
Rust ownership/layout tests without modifying bundled Lib sources.

Spell the set/frozenset union owner directly as PyRef<PyAnySet> without
a second owned alias; retain the sealed internal view registration.

Assisted-by: OpenAI Codex:model identifier unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-13 14:11:37 +09:00
1 parent 85edb52b65
commit 8b66b7c48f
12 files changed
+763 -253

No files matched your search

Generated
+1
View File
@@ -3725,6 +3725,7 @@ dependencies = [
"rustpython-pylib",
"rustpython-stdlib",
"rustpython-vm",
"static_assertions",
]
[[package]]
+1
View File
@@ -22,6 +22,7 @@ rustpython-stdlib = {workspace = true, features = ["threading"] }
rustpython-pylib = { workspace = true }
[dev-dependencies]
static_assertions = { workspace = true }
pyo3 = { workspace = true, features = ["auto-initialize", "abi3t"] }
[lints]
-1
View File
@@ -5,7 +5,6 @@ use core::convert::Infallible;
use core::ffi::{c_char, c_int};
use core::slice;
use rustpython_vm::builtins::{PyBaseException, PyTraceback, PyTuple, PyType};
use rustpython_vm::convert::IntoObject;
use rustpython_vm::exceptions::ExceptionZoo;
use rustpython_vm::{AsObject, PyResult};
+11 -11
View File
@@ -40,20 +40,12 @@ where
impl<T> FfiResult<*mut Py<T>> for PyRef<T>
where
Self: Into<PyObjectRef>,
T: rustpython_vm::object::PyRefTarget<Target = Py<T>>,
{
const ERR_VALUE: *mut Py<T> = core::ptr::null_mut();
fn into_output(self, _vm: &VirtualMachine) -> *mut Py<T> {
self.into().into_raw().as_ptr().cast()
}
}
impl FfiResult<*mut PyObject> for PyObjectRef {
const ERR_VALUE: *mut PyObject = core::ptr::null_mut();
fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject {
self.into_raw().as_ptr()
PyObjectRef::from(self).into_raw().as_ptr().cast()
}
}
@@ -324,7 +316,7 @@ impl<T: PyPayload> FfiPtrExt for *mut Py<T> {
"Attempted to dereference NULL {}",
type_name::<Self>()
);
unsafe { PyRef::from_raw(self.cast_const()) }
unsafe { PyRef::<T>::from_raw(self.cast_const()) }
}
#[inline]
@@ -384,6 +376,14 @@ mod tests {
use core::ffi::{c_longlong, c_ulonglong};
use core::fmt::Debug;
#[test]
fn pyref_object_ffi_targets() {
use rustpython_vm::builtins::PyInt;
static_assertions::assert_impl_all!(PyObjectRef: FfiResult<*mut PyObject>);
static_assertions::assert_impl_all!(PyRef<PyInt>: FfiResult<*mut PyObject>, FfiResult<*mut Py<PyInt>>);
static_assertions::assert_not_impl_any!(PyObjectRef: FfiResult<*mut Py<PyObject>>);
}
#[test]
fn ffi_result_err_value() {
fn assert_error_value<T, Output>(value: Output)
-1
View File
@@ -61,7 +61,6 @@ mod _sqlite3 {
PyInt, PyIntRef, PyModule, PySlice, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType,
PyTypeRef, PyUtf8Str, PyUtf8StrRef,
},
convert::IntoObject,
function::{
ArgCallable, ArgIterable, FsPath, FuncArgs, KwArgs, OptionalArg, PyComparisonValue,
PySetterValue, TimeoutSeconds,
-6
View File
@@ -543,12 +543,6 @@ impl MemberCell for core::sync::atomic::AtomicU32 {}
// One object pointer. Readonly members may be a plain pointer. Writable
// members are an atomic cell: `PyAtomicRef<PyObject>` when the pointer is
// never null, or `PyAtomicRef<Option<PyObject>>` when it may be.
impl MemberLayout for PyObjectRef {
const KIND: MemberKind = MemberKind::Object;
}
impl MemberLayout for Option<PyObjectRef> {
const KIND: MemberKind = MemberKind::Object;
}
impl MemberLayout for crate::object::PyObjectCell {
const KIND: MemberKind = MemberKind::Object;
const ATOMIC: bool = true;
+2 -2
View File
@@ -145,10 +145,10 @@ impl FrameLocalsProxy {
return Ok(vm.ctx.not_implemented());
}
let result = self.snapshot(vm)?;
if other.downcast_ref::<PyDict>().is_some() {
if let Some(dict) = other.downcast_ref::<PyDict>() {
// PyDict_Update reads a dict subclass's stored entries directly;
// it does not dispatch to overridden mapping methods.
result.merge_dict(&other.downcast().unwrap(), true, vm)?;
result.merge_dict(dict, true, vm)?;
} else {
result.merge_object(other, vm)?;
}
+296 -69
View File
@@ -16,7 +16,7 @@ use crate::{
rc::PyRc,
wtf8::Wtf8Buf,
},
convert::ToPyResult,
convert::{ToPyResult, TransmuteFromObject},
dict_inner::{self, DictSize},
function::{
ArgIterable, FuncArgs, NameOthers, OptionalArg, PosArgs, PyArithmeticValue,
@@ -70,7 +70,7 @@ impl PySet {
fn op(
&self,
other: AnySet,
other: PyRef<PyAnySet>,
op: fn(&PySetInner, ArgIterable, &VirtualMachine) -> PyResult<PySetInner>,
vm: &VirtualMachine,
) -> PyResult<Self> {
@@ -132,7 +132,7 @@ impl PyFrozenSet {
fn op(
&self,
other: AnySet,
other: PyRef<PyAnySet>,
op: fn(&PySetInner, ArgIterable, &VirtualMachine) -> PyResult<PySetInner>,
vm: &VirtualMachine,
) -> PyResult<Self> {
@@ -537,14 +537,14 @@ impl PySetInner {
}
fn update_internal(&self, iterable: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
// check AnySet
if let Ok(any_set) = AnySet::try_from_object(vm, iterable.to_owned()) {
// check set or frozenset
if let Ok(any_set) = PyRef::<PyAnySet>::try_from_object(vm, iterable.to_owned()) {
self.merge_set(any_set, vm)
// check Dict
} else if let Ok(dict) = iterable.to_owned().downcast_exact::<PyDict>(vm) {
self.merge_dict(&dict, vm)
} else {
// add iterable that is not AnySet or Dict
// add iterable that is not a set, frozenset, or dict
for item in iterable.try_into_value::<ArgIterable>(vm)?.iter(vm)? {
let item = item?;
self.add(&item, vm)?;
@@ -553,7 +553,7 @@ impl PySetInner {
}
}
fn merge_set(&self, any_set: AnySet, vm: &VirtualMachine) -> PyResult<()> {
fn merge_set(&self, any_set: PyRef<PyAnySet>, vm: &VirtualMachine) -> PyResult<()> {
for (item, hash) in any_set.as_inner().content.keys_with_hashes() {
self.add_known_hash(&item, hash, vm)?;
}
@@ -752,7 +752,7 @@ impl PySet {
}
fn __or__(&self, other: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyArithmeticValue<Self>> {
if let Ok(other) = AnySet::try_from_object(vm, other) {
if let Ok(other) = PyRef::<PyAnySet>::try_from_object(vm, other) {
Ok(PyArithmeticValue::Implemented(self.op(
other,
PySetInner::union,
@@ -768,7 +768,7 @@ impl PySet {
other: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyArithmeticValue<Self>> {
if let Ok(other) = AnySet::try_from_object(vm, other) {
if let Ok(other) = PyRef::<PyAnySet>::try_from_object(vm, other) {
Ok(PyArithmeticValue::Implemented(Self {
inner: self.inner.intersection(other.into_iterable(vm)?, vm)?,
}))
@@ -782,7 +782,7 @@ impl PySet {
other: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyArithmeticValue<Self>> {
if let Ok(other) = AnySet::try_from_object(vm, other) {
if let Ok(other) = PyRef::<PyAnySet>::try_from_object(vm, other) {
Ok(PyArithmeticValue::Implemented(Self {
inner: self.inner.difference_new(other.into_iterable(vm)?, vm)?,
}))
@@ -796,7 +796,7 @@ impl PySet {
other: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyArithmeticValue<Self>> {
if let Ok(other) = AnySet::try_from_object(vm, other) {
if let Ok(other) = PyRef::<PyAnySet>::try_from_object(vm, other) {
Ok(PyArithmeticValue::Implemented(Self {
inner: other
.as_inner()
@@ -812,7 +812,7 @@ impl PySet {
other: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyArithmeticValue<Self>> {
if let Ok(other) = AnySet::try_from_object(vm, other) {
if let Ok(other) = PyRef::<PyAnySet>::try_from_object(vm, other) {
Ok(PyArithmeticValue::Implemented(self.op(
other,
PySetInner::symmetric_difference,
@@ -823,12 +823,20 @@ impl PySet {
}
}
fn __ior__(zelf: PyRef<Self>, set: AnySet, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
fn __ior__(
zelf: PyRef<Self>,
set: PyRef<PyAnySet>,
vm: &VirtualMachine,
) -> PyResult<PyRef<Self>> {
zelf.inner.merge_set(set, vm)?;
Ok(zelf)
}
fn __iand__(zelf: PyRef<Self>, set: AnySet, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
fn __iand__(
zelf: PyRef<Self>,
set: PyRef<PyAnySet>,
vm: &VirtualMachine,
) -> PyResult<PyRef<Self>> {
if !set.is(zelf.as_object()) {
zelf.inner
.intersection_update(core::iter::once(set.into_iterable(vm)?), vm)?;
@@ -836,7 +844,11 @@ impl PySet {
Ok(zelf)
}
fn __isub__(zelf: PyRef<Self>, set: AnySet, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
fn __isub__(
zelf: PyRef<Self>,
set: PyRef<PyAnySet>,
vm: &VirtualMachine,
) -> PyResult<PyRef<Self>> {
if set.is(zelf.as_object()) {
zelf.inner.clear();
} else {
@@ -846,7 +858,11 @@ impl PySet {
Ok(zelf)
}
fn __ixor__(zelf: PyRef<Self>, set: AnySet, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
fn __ixor__(
zelf: PyRef<Self>,
set: PyRef<PyAnySet>,
vm: &VirtualMachine,
) -> PyResult<PyRef<Self>> {
if set.is(zelf.as_object()) {
zelf.inner.clear();
} else {
@@ -1068,10 +1084,7 @@ impl Comparable for PySet {
impl Iterable for PySet {
fn iter(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult {
Ok(PySetIterator::new(AnySet {
object: zelf.into(),
})
.into_pyobject(vm))
Ok(PySetIterator::new(zelf.into()).into_pyobject(vm))
}
}
@@ -1081,7 +1094,7 @@ impl AsNumber for PySet {
// Binary ops check both operands are sets (like CPython's set_sub, etc.)
// This is needed because __rsub__ swaps operands: a.__rsub__(b) calls subtract(b, a)
subtract: Some(|a, b, vm| {
if !AnySet::check(a, vm) || !AnySet::check(b, vm) {
if !PyRef::<PyAnySet>::check(a, vm) || !PyRef::<PyAnySet>::check(b, vm) {
return Ok(vm.ctx.not_implemented());
}
if let Some(a) = a.downcast_ref::<PySet>() {
@@ -1096,7 +1109,7 @@ impl AsNumber for PySet {
}
}),
and: Some(|a, b, vm| {
if !AnySet::check(a, vm) || !AnySet::check(b, vm) {
if !PyRef::<PyAnySet>::check(a, vm) || !PyRef::<PyAnySet>::check(b, vm) {
return Ok(vm.ctx.not_implemented());
}
if let Some(a) = a.downcast_ref::<PySet>() {
@@ -1110,7 +1123,7 @@ impl AsNumber for PySet {
}
}),
xor: Some(|a, b, vm| {
if !AnySet::check(a, vm) || !AnySet::check(b, vm) {
if !PyRef::<PyAnySet>::check(a, vm) || !PyRef::<PyAnySet>::check(b, vm) {
return Ok(vm.ctx.not_implemented());
}
if let Some(a) = a.downcast_ref::<PySet>() {
@@ -1124,7 +1137,7 @@ impl AsNumber for PySet {
}
}),
or: Some(|a, b, vm| {
if !AnySet::check(a, vm) || !AnySet::check(b, vm) {
if !PyRef::<PyAnySet>::check(a, vm) || !PyRef::<PyAnySet>::check(b, vm) {
return Ok(vm.ctx.not_implemented());
}
if let Some(a) = a.downcast_ref::<PySet>() {
@@ -1139,32 +1152,48 @@ impl AsNumber for PySet {
}),
inplace_subtract: Some(|a, b, vm| {
if let Some(a) = a.downcast_ref::<PySet>() {
PySet::__isub__(a.to_owned(), AnySet::try_from_object(vm, b.to_owned())?, vm)
.to_pyresult(vm)
PySet::__isub__(
a.to_owned(),
PyRef::<PyAnySet>::try_from_object(vm, b.to_owned())?,
vm,
)
.to_pyresult(vm)
} else {
Ok(vm.ctx.not_implemented())
}
}),
inplace_and: Some(|a, b, vm| {
if let Some(a) = a.downcast_ref::<PySet>() {
PySet::__iand__(a.to_owned(), AnySet::try_from_object(vm, b.to_owned())?, vm)
.to_pyresult(vm)
PySet::__iand__(
a.to_owned(),
PyRef::<PyAnySet>::try_from_object(vm, b.to_owned())?,
vm,
)
.to_pyresult(vm)
} else {
Ok(vm.ctx.not_implemented())
}
}),
inplace_xor: Some(|a, b, vm| {
if let Some(a) = a.downcast_ref::<PySet>() {
PySet::__ixor__(a.to_owned(), AnySet::try_from_object(vm, b.to_owned())?, vm)
.to_pyresult(vm)
PySet::__ixor__(
a.to_owned(),
PyRef::<PyAnySet>::try_from_object(vm, b.to_owned())?,
vm,
)
.to_pyresult(vm)
} else {
Ok(vm.ctx.not_implemented())
}
}),
inplace_or: Some(|a, b, vm| {
if let Some(a) = a.downcast_ref::<PySet>() {
PySet::__ior__(a.to_owned(), AnySet::try_from_object(vm, b.to_owned())?, vm)
.to_pyresult(vm)
PySet::__ior__(
a.to_owned(),
PyRef::<PyAnySet>::try_from_object(vm, b.to_owned())?,
vm,
)
.to_pyresult(vm)
} else {
Ok(vm.ctx.not_implemented())
}
@@ -1279,7 +1308,7 @@ impl PyFrozenSet {
}
fn __or__(&self, other: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyArithmeticValue<Self>> {
if let Ok(set) = AnySet::try_from_object(vm, other) {
if let Ok(set) = PyRef::<PyAnySet>::try_from_object(vm, other) {
Ok(PyArithmeticValue::Implemented(self.op(
set,
PySetInner::union,
@@ -1295,7 +1324,7 @@ impl PyFrozenSet {
other: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyArithmeticValue<Self>> {
if let Ok(other) = AnySet::try_from_object(vm, other) {
if let Ok(other) = PyRef::<PyAnySet>::try_from_object(vm, other) {
Ok(PyArithmeticValue::Implemented(Self {
inner: self.inner.intersection(other.into_iterable(vm)?, vm)?,
..Default::default()
@@ -1310,7 +1339,7 @@ impl PyFrozenSet {
other: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyArithmeticValue<Self>> {
if let Ok(other) = AnySet::try_from_object(vm, other) {
if let Ok(other) = PyRef::<PyAnySet>::try_from_object(vm, other) {
Ok(PyArithmeticValue::Implemented(Self {
inner: self.inner.difference_new(other.into_iterable(vm)?, vm)?,
..Default::default()
@@ -1325,7 +1354,7 @@ impl PyFrozenSet {
other: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyArithmeticValue<Self>> {
if let Ok(other) = AnySet::try_from_object(vm, other) {
if let Ok(other) = PyRef::<PyAnySet>::try_from_object(vm, other) {
Ok(PyArithmeticValue::Implemented(Self {
inner: other
.as_inner()
@@ -1342,7 +1371,7 @@ impl PyFrozenSet {
other: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyArithmeticValue<Self>> {
if let Ok(other) = AnySet::try_from_object(vm, other) {
if let Ok(other) = PyRef::<PyAnySet>::try_from_object(vm, other) {
Ok(PyArithmeticValue::Implemented(self.op(
other,
PySetInner::symmetric_difference,
@@ -1520,10 +1549,7 @@ impl Comparable for PyFrozenSet {
impl Iterable for PyFrozenSet {
fn iter(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult {
Ok(PySetIterator::new(AnySet {
object: zelf.into(),
})
.into_pyobject(vm))
Ok(PySetIterator::new(zelf.into()).into_pyobject(vm))
}
}
@@ -1533,7 +1559,7 @@ impl AsNumber for PyFrozenSet {
// Binary ops check both operands are sets (like CPython's set_sub, etc.)
// __rsub__ swaps operands. Result type follows first operand's type.
subtract: Some(|a, b, vm| {
if !AnySet::check(a, vm) || !AnySet::check(b, vm) {
if !PyRef::<PyAnySet>::check(a, vm) || !PyRef::<PyAnySet>::check(b, vm) {
return Ok(vm.ctx.not_implemented());
}
if let Some(a) = a.downcast_ref::<PyFrozenSet>() {
@@ -1546,7 +1572,7 @@ impl AsNumber for PyFrozenSet {
}
}),
and: Some(|a, b, vm| {
if !AnySet::check(a, vm) || !AnySet::check(b, vm) {
if !PyRef::<PyAnySet>::check(a, vm) || !PyRef::<PyAnySet>::check(b, vm) {
return Ok(vm.ctx.not_implemented());
}
if let Some(a) = a.downcast_ref::<PyFrozenSet>() {
@@ -1558,7 +1584,7 @@ impl AsNumber for PyFrozenSet {
}
}),
xor: Some(|a, b, vm| {
if !AnySet::check(a, vm) || !AnySet::check(b, vm) {
if !PyRef::<PyAnySet>::check(a, vm) || !PyRef::<PyAnySet>::check(b, vm) {
return Ok(vm.ctx.not_implemented());
}
if let Some(a) = a.downcast_ref::<PyFrozenSet>() {
@@ -1570,7 +1596,7 @@ impl AsNumber for PyFrozenSet {
}
}),
or: Some(|a, b, vm| {
if !AnySet::check(a, vm) || !AnySet::check(b, vm) {
if !PyRef::<PyAnySet>::check(a, vm) || !PyRef::<PyAnySet>::check(b, vm) {
return Ok(vm.ctx.not_implemented());
}
if let Some(a) = a.downcast_ref::<PyFrozenSet>() {
@@ -1604,27 +1630,78 @@ impl Representable for PyFrozenSet {
}
}
struct AnySet {
object: PyObjectRef,
/// A borrowed view of either a set or a frozenset, including subclasses.
///
/// This is an erased header view, not a shared payload layout: every payload
/// access still downcasts to the actual alternative. It cannot be allocated as
/// a payload or constructed without checking the object.
#[repr(transparent)]
pub(crate) struct PyAnySet {
object: PyObject,
}
impl Borrow<PyObject> for AnySet {
impl Borrow<PyObject> for PyAnySet {
#[inline(always)]
fn borrow(&self) -> &PyObject {
&self.object
}
}
impl AnySet {
/// Check if object is a set or frozenset (including subclasses)
/// Equivalent to CPython's PyAnySet_Check
impl Borrow<PyAnySet> for PyRef<PyAnySet> {
fn borrow(&self) -> &PyAnySet {
self
}
}
impl ToOwned for PyAnySet {
type Owned = PyRef<Self>;
fn to_owned(&self) -> PyRef<Self> {
// SAFETY: this borrowed view already guarantees a set payload.
unsafe { PyRef::<Self>::from_object_unchecked(self.object.to_owned()) }
}
}
impl From<PyRef<PySet>> for PyRef<PyAnySet> {
fn from(set: PyRef<PySet>) -> Self {
// SAFETY: the typed owner guarantees the set alternative.
unsafe { Self::from_object_unchecked(set.into()) }
}
}
impl From<PyRef<PyFrozenSet>> for PyRef<PyAnySet> {
fn from(set: PyRef<PyFrozenSet>) -> Self {
// SAFETY: the typed owner guarantees the frozenset alternative.
unsafe { Self::from_object_unchecked(set.into()) }
}
}
impl From<PyRef<PyAnySet>> for PyObjectRef {
fn from(set: PyRef<PyAnySet>) -> Self {
set.into_object()
}
}
impl PyAnySet {
/// Check if object is a set or frozenset (including subclasses).
/// Equivalent to CPython's PyAnySet_Check.
fn check(obj: &PyObject, vm: &VirtualMachine) -> bool {
let ctx = &vm.ctx;
obj.fast_isinstance(ctx.types.set_type) || obj.fast_isinstance(ctx.types.frozenset_type)
}
fn as_inner(&self) -> &PySetInner {
extract_set(&self.object).expect("PyAnySet is always PySet or PyFrozenSet")
}
}
impl PyRef<PyAnySet> {
fn check(obj: &PyObject, vm: &VirtualMachine) -> bool {
PyAnySet::check(obj, vm)
}
fn into_iterable(self, vm: &VirtualMachine) -> PyResult<ArgIterable> {
self.object.try_into_value(vm)
self.into_object().try_into_value(vm)
}
fn into_iterable_iter(
@@ -1633,26 +1710,28 @@ impl AnySet {
) -> PyResult<impl core::iter::Iterator<Item = ArgIterable>> {
Ok(core::iter::once(self.into_iterable(vm)?))
}
}
fn as_inner(&self) -> &PySetInner {
match_class!(match self.object.as_object() {
ref set @ PySet => &set.inner,
ref frozen @ PyFrozenSet => &frozen.inner,
_ => unreachable!("AnySet is always PySet or PyFrozenSet"), // should not be called.
})
// SAFETY: PyRef<PyAnySet> is a transparent PyRef owner. A successful check proves
// both the Python class and the actual set/frozenset payload of this object.
unsafe impl TransmuteFromObject for PyRef<PyAnySet> {
fn check(vm: &VirtualMachine, obj: &PyObject) -> PyResult<()> {
if PyAnySet::check(obj, vm) && extract_set(obj).is_some() {
Ok(())
} else {
Err(vm.new_type_error(format!(
"{} is not a subtype of set or frozenset",
obj.class()
)))
}
}
}
impl TryFromObject for AnySet {
impl TryFromObject for PyRef<PyAnySet> {
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
let class = obj.class();
if class.fast_issubclass(vm.ctx.types.set_type)
|| class.fast_issubclass(vm.ctx.types.frozenset_type)
{
Ok(Self { object: obj })
} else {
Err(vm.new_type_error(format!("{class} is not a subtype of set or frozenset")))
}
<Self as TransmuteFromObject>::check(vm, &obj)?;
// SAFETY: the shared checker proved the union view's payload invariant.
Ok(unsafe { Self::from_object_unchecked(obj) })
}
}
@@ -1663,7 +1742,7 @@ pub(crate) struct PySetIterator {
/// records by writing a size no set can have. Sticky: what it makes the
/// iterator answer, it answers from then on.
changed: PyAtomic<bool>,
internal: PyMutex<PositionIterInternal<AnySet>>,
internal: PyMutex<PositionIterInternal<PyRef<PyAnySet>>>,
}
impl fmt::Debug for PySetIterator {
@@ -1681,7 +1760,7 @@ impl PyPayload for PySetIterator {
}
impl PySetIterator {
fn new(set: AnySet) -> Self {
fn new(set: PyRef<PyAnySet>) -> Self {
Self {
size: set.as_inner().content.size(),
changed: Radium::new(false),
@@ -1811,3 +1890,151 @@ pub(crate) fn init(context: &'static Context) {
PySetIterator::extend_class(context, context.types.set_iterator_type);
}
#[cfg(test)]
mod reference_tests {
use super::*;
#[test]
fn anyset_reference_layout_and_ownership() {
static_assertions::assert_not_impl_any!(PyAnySet: PyPayload);
static_assertions::assert_type_eq_all!(
<PyAnySet as crate::object::PyRefTarget>::Target,
PyAnySet
);
static_assertions::assert_eq_size!(PyRef<PyAnySet>, PyObjectRef, Option<PyRef<PyAnySet>>);
static_assertions::assert_eq_align!(PyRef<PyAnySet>, PyObjectRef);
static_assertions::assert_eq_size!(PyAnySet, PyObject);
static_assertions::assert_eq_align!(PyAnySet, PyObject);
#[cfg(feature = "threading")]
static_assertions::assert_impl_all!(PyRef<PyAnySet>: Send, Sync);
#[cfg(not(feature = "threading"))]
static_assertions::assert_not_impl_any!(PyRef<PyAnySet>: Send, Sync);
let ctx = Context::genesis();
let set: PyRef<PyAnySet> = PySet::default().into_ref(ctx).into();
let frozen: PyRef<PyAnySet> = PyFrozenSet::default().into_ref(ctx).into();
for owner in [set, frozen] {
let object = owner.as_object() as *const PyObject;
assert_eq!(owner.as_object().strong_count(), 1);
let borrowed: &PyAnySet = &owner;
assert!(core::ptr::eq(borrowed.as_object(), object));
assert!(core::ptr::eq(
borrowed.as_inner(),
extract_set(owner.as_object()).unwrap()
));
let copy = borrowed.to_owned();
assert_eq!(owner.as_object().strong_count(), 2);
let clone = owner.clone();
assert_eq!(owner.as_object().strong_count(), 3);
let erased = clone.into_object();
assert_eq!(erased.strong_count(), 3);
assert!(core::ptr::eq(&*erased, object));
drop(copy);
drop(owner);
assert_eq!(erased.strong_count(), 1);
}
}
#[test]
fn anyset_checked_reference_conversion() {
crate::Interpreter::without_stdlib(Default::default()).enter(|vm| {
let set = PySet::default().into_pyobject(vm);
let frozen = PyFrozenSet::default().into_pyobject(vm);
for object in [set, frozen] {
let owner = PyRef::<PyAnySet>::try_from_object(vm, object.clone()).unwrap();
assert_eq!(object.strong_count(), 2);
assert!(core::ptr::eq(owner.as_object(), &*object));
let erased: PyObjectRef = owner.into();
assert_eq!(object.strong_count(), 2);
assert!(core::ptr::eq(&*erased, &*object));
drop(erased);
assert_eq!(object.strong_count(), 1);
}
let unrelated: PyObjectRef = vm.ctx.new_int(1000).into();
let error = PyRef::<PyAnySet>::try_from_object(vm, unrelated.clone()).unwrap_err();
assert!(error.fast_isinstance(vm.ctx.exceptions.type_error));
assert_eq!(unrelated.strong_count(), 1);
});
}
#[test]
fn anyset_typed_tuple_views_and_refcounts() {
use crate::builtins::PyTuple;
type Typed = PyTuple<PyRef<PyAnySet>>;
type Nested = PyTuple<PyRef<Typed>>;
static_assertions::assert_not_impl_any!(PyObject: crate::object::PyTypedRefTarget);
static_assertions::assert_impl_all!(PyAnySet: crate::object::PyTypedRefTarget);
static_assertions::assert_impl_all!(Typed: crate::object::PyTypedRefTarget);
static_assertions::assert_eq_size!(Typed, PyTuple);
static_assertions::assert_eq_align!(Typed, PyTuple);
static_assertions::assert_eq_size!(Py<Typed>, Py<PyTuple>);
static_assertions::assert_eq_align!(Py<Typed>, Py<PyTuple>);
crate::Interpreter::without_stdlib(Default::default()).enter(|vm| {
let set: PyRef<PyAnySet> = PySet::default().into_ref(&vm.ctx).into();
let frozen: PyRef<PyAnySet> = PyFrozenSet::default().into_ref(&vm.ctx).into();
let tuple = Typed::new_ref_typed(vec![set.clone(), frozen.clone()], &vm.ctx);
assert_eq!(set.as_object().strong_count(), 2);
assert_eq!(frozen.as_object().strong_count(), 2);
assert!(
tuple.as_slice()[0]
.as_object()
.downcast_ref::<PySet>()
.is_some()
);
assert!(
tuple.as_slice()[1]
.as_object()
.downcast_ref::<PyFrozenSet>()
.is_some()
);
assert!(core::ptr::eq(
tuple.as_slice()[0].as_inner(),
set.as_inner()
));
assert!(core::ptr::eq(
tuple.as_slice()[1].as_inner(),
frozen.as_inner()
));
let erased = tuple.clone().into_untyped();
assert!(core::ptr::eq(tuple.as_untyped(), &*erased));
assert_eq!(erased.as_object().strong_count(), 2);
let checked = erased.try_into_typed::<PyAnySet>(vm).unwrap();
assert!(core::ptr::eq(&*checked, &*tuple));
assert_eq!(checked.as_object().strong_count(), 2);
let borrowed_copy = (*checked).to_owned();
assert_eq!(tuple.as_object().strong_count(), 3);
let nested: PyRef<Nested> = PyTuple::new_ref_typed(vec![borrowed_copy], &vm.ctx);
assert!(core::ptr::eq(&*nested.as_slice()[0], &*tuple));
let untyped: PyTupleRef = nested.into();
assert_eq!(tuple.as_object().strong_count(), 3);
drop(untyped);
drop(checked);
assert_eq!(tuple.as_object().strong_count(), 1);
drop(tuple);
assert_eq!(set.as_object().strong_count(), 1);
assert_eq!(frozen.as_object().strong_count(), 1);
});
}
#[test]
fn anyset_typed_tuple_rejects_invalid_element_without_leaking() {
use crate::builtins::PyTuple;
crate::Interpreter::without_stdlib(Default::default()).enter(|vm| {
let set = PySet::default().into_pyobject(vm);
let wrong: PyObjectRef = vm.ctx.new_int(1000).into();
let tuple = PyTuple::new_ref(vec![set.clone(), wrong.clone()], &vm.ctx);
let error = tuple.clone().try_into_typed::<PyAnySet>(vm).unwrap_err();
assert!(error.fast_isinstance(vm.ctx.exceptions.type_error));
assert_eq!(tuple.as_object().strong_count(), 1);
assert!(core::ptr::eq(&*tuple.as_slice()[0], &*set));
assert!(core::ptr::eq(&*tuple.as_slice()[1], &*wrong));
assert_eq!(set.strong_count(), 2);
assert_eq!(wrong.strong_count(), 2);
drop(tuple);
assert_eq!(set.strong_count(), 1);
assert_eq!(wrong.strong_count(), 1);
});
}
}
+91 -46
View File
@@ -4,7 +4,7 @@ use super::{
};
use crate::common::lock::LazyLock;
use crate::common::{hash, hash::PyHash, lock::PyMutex, wtf8::wtf8_concat};
use crate::object::{Traverse, TraverseFn};
use crate::object::{PyTypedRefTarget, Traverse, TraverseFn};
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
atomic_func,
@@ -25,9 +25,12 @@ use crate::{
};
use alloc::fmt;
use core::cell::{Cell, UnsafeCell};
use core::marker::PhantomData;
use core::mem::ManuallyDrop;
use core::ptr::NonNull;
#[pyclass(module = false, name = "tuple", traverse = "manual")]
#[repr(transparent)]
pub struct PyTuple<R = PyObjectRef> {
elements: TupleElements<R>,
}
@@ -35,30 +38,41 @@ pub struct PyTuple<R = PyObjectRef> {
/// Tuple storage is immutable after publication, but marshal must publish a
/// tuple in its reference table before recursively reading its children.
/// This mirrors CPython's `PyTuple_New` followed by `PyTuple_SET_ITEM`.
struct TupleElements<R>(UnsafeCell<Box<[R]>>);
// Every allocation stores erased owners, including when borrowed through a
// typed tuple view. The marker preserves the old storage's invariance and
// ownership relationship to R.
// Safe constructors and checked view conversions only admit R = PyRef<T>,
// and establish each element's target invariant before publishing that view.
#[repr(transparent)]
struct TupleElements<R>(UnsafeCell<Box<[PyObjectRef]>>, PhantomData<UnsafeCell<R>>);
unsafe impl<R: Send> Send for TupleElements<R> {}
unsafe impl<R: Sync> Sync for TupleElements<R> {}
impl<R> TupleElements<R> {
const fn new(elements: Box<[R]>) -> Self {
Self(UnsafeCell::new(elements))
}
fn as_slice(&self) -> &[R] {
// SAFETY: initialization writes happen only while the tuple is owned by
// the synchronous marshal decoder; afterwards the storage is immutable.
unsafe { &*self.0.get() }
// SAFETY: all admitted R are transparent PyRef owners with the same
// size/alignment as PyObjectRef. Their element invariants were proven
// by construction or checked conversion. Published elements cannot be
// replaced, and the returned shared slice borrows this storage.
let elements = unsafe { &*self.0.get() };
unsafe { core::slice::from_raw_parts(elements.as_ptr().cast(), elements.len()) }
}
}
impl TupleElements<PyObjectRef> {
const fn new(elements: Box<[PyObjectRef]>) -> Self {
Self(UnsafeCell::new(elements), PhantomData)
}
fn get_mut(&mut self) -> &mut Box<[R]> {
fn get_mut(&mut self) -> &mut Box<[PyObjectRef]> {
self.0.get_mut()
}
/// # Safety
/// The tuple must still be in its private initialization phase, and each
/// placeholder index must be replaced at most once before it is observable.
unsafe fn set_initializing(&self, index: usize, value: R) {
unsafe fn set_initializing(&self, index: usize, value: PyObjectRef) {
unsafe { (*self.0.get())[index] = value };
}
}
@@ -398,6 +412,38 @@ impl<R> Py<PyTuple<R>> {
}
impl Py<PyTuple> {
fn _getitem(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult {
match SequenceIndex::try_from_borrowed_object::<NameTuple>(vm, needle)? {
SequenceIndex::Int(i) => {
let index = self
.elements
.wrap_index(i)
.ok_or_else(|| vm.new_index_error("tuple index out of range"))?;
Ok(self.elements[index].clone())
}
SequenceIndex::Slice(slice) => {
let len = self.as_slice().len();
let (range, step, slice_len) = slice.adjust_indices(len);
if slice_len != 0
&& range.start == 0
&& step == 1
&& slice_len == len
&& self.class().is(vm.ctx.types.tuple_type)
{
Ok(self.to_owned().into())
} else {
self.elements
.getitem_by_slice(vm, slice)
.map(|x| vm.ctx.new_tuple(x).into())
}
}
}
}
fn __getitem__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult {
self._getitem(needle, vm)
}
pub fn extract_tuple<'a, T: FromPyTuple<'a>>(&'a self, vm: &VirtualMachine) -> PyResult<T> {
T::from_pytuple(self, vm)
}
@@ -410,8 +456,14 @@ impl<T> PyTuple<PyRef<T>> {
) -> PyRef<Self> {
// SAFETY: PyRef<T> has the same layout as PyObjectRef.
unsafe {
let elements: Vec<PyObjectRef> =
core::mem::transmute::<Vec<PyRef<T>>, Vec<PyObjectRef>>(elements);
let mut elements = ManuallyDrop::new(elements);
// Preserve the buffer, length and capacity without assuming Vec's
// field order. Each transparent owner transfers its one reference.
let elements = Vec::from_raw_parts(
elements.as_mut_ptr().cast::<PyObjectRef>(),
elements.len(),
elements.capacity(),
);
let tuple = PyRef::new_ref(
PyTuple::new_unchecked(elements.into_boxed_slice()),
tuple_type,
@@ -424,8 +476,14 @@ impl<T> PyTuple<PyRef<T>> {
pub fn new_ref_typed(elements: Vec<PyRef<T>>, ctx: &Context) -> PyRef<Self> {
// SAFETY: PyRef<T> has the same layout as PyObjectRef
unsafe {
let elements: Vec<PyObjectRef> =
core::mem::transmute::<Vec<PyRef<T>>, Vec<PyObjectRef>>(elements);
let mut elements = ManuallyDrop::new(elements);
// Preserve the buffer, length and capacity without assuming Vec's
// field order. Each transparent owner transfers its one reference.
let elements = Vec::from_raw_parts(
elements.as_mut_ptr().cast::<PyObjectRef>(),
elements.len(),
elements.capacity(),
);
let tuple = PyTuple::<PyObjectRef>::new_ref(elements, ctx);
core::mem::transmute::<PyRef<PyTuple>, PyRef<Self>>(tuple)
}
@@ -469,26 +527,6 @@ impl PyTuple {
Self::repeat(zelf, value, vm)
}
fn _getitem(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult {
match SequenceIndex::try_from_borrowed_object::<NameTuple>(vm, needle)? {
SequenceIndex::Int(i) => {
let index = self
.elements
.wrap_index(i)
.ok_or_else(|| vm.new_index_error("tuple index out of range"))?;
Ok(self.elements[index].clone())
}
SequenceIndex::Slice(slice) => self
.elements
.getitem_by_slice(vm, slice)
.map(|x| vm.ctx.new_tuple(x).into()),
}
}
fn __getitem__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult {
self._getitem(needle, vm)
}
fn _contains(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
for element in &self.elements {
if vm.identical_or_equal(element, needle)? {
@@ -565,9 +603,9 @@ impl AsMapping for PyTuple {
length: atomic_func!(|mapping, _vm| {
Ok(PyTuple::mapping_downcast(mapping).as_slice().len())
}),
subscript: atomic_func!(|mapping, needle, vm| PyTuple::mapping_downcast(mapping)
.payload
._getitem(needle, vm)),
subscript: atomic_func!(
|mapping, needle, vm| PyTuple::mapping_downcast(mapping)._getitem(needle, vm)
),
..PyMappingMethods::NOT_IMPLEMENTED
});
&AS_MAPPING
@@ -686,35 +724,42 @@ impl Representable for PyTuple {
}
impl PyRef<PyTuple<PyObjectRef>> {
pub fn try_into_typed<T: PyPayload>(
pub fn try_into_typed<T: PyTypedRefTarget>(
self,
vm: &VirtualMachine,
) -> PyResult<PyRef<PyTuple<PyRef<T>>>> {
) -> PyResult<PyRef<PyTuple<PyRef<T>>>>
where
PyRef<T>: TransmuteFromObject,
{
// Check that all elements are of the correct type
for elem in self.as_slice() {
<PyRef<T> as TransmuteFromObject>::check(vm, elem)?;
}
// SAFETY: We just verified all elements are of type T
// SAFETY: every element satisfies T's view invariant. Transparent
// tuple wrappers retain the same erased storage for every marker;
// this transfers the owner without changing its allocation or count.
Ok(unsafe { core::mem::transmute::<Self, PyRef<PyTuple<PyRef<T>>>>(self) })
}
}
impl<T: PyPayload> PyRef<PyTuple<PyRef<T>>> {
impl<T: PyTypedRefTarget> PyRef<PyTuple<PyRef<T>>> {
#[must_use]
pub fn into_untyped(self) -> PyRef<PyTuple> {
// SAFETY: PyTuple<PyRef<T>> has the same layout as PyTuple
// SAFETY: transparent tuple wrappers have identical erased storage;
// forgetting the element guarantee does not change the allocation.
unsafe { core::mem::transmute::<Self, PyRef<PyTuple>>(self) }
}
}
impl<T: PyPayload> Py<PyTuple<PyRef<T>>> {
impl<T: PyTypedRefTarget> Py<PyTuple<PyRef<T>>> {
pub fn as_untyped(&self) -> &Py<PyTuple> {
// SAFETY: PyTuple<PyRef<T>> has the same layout as PyTuple
// SAFETY: transparent tuple wrappers have identical erased storage;
// forgetting the element guarantee does not change the allocation.
unsafe { core::mem::transmute::<&Self, &Py<PyTuple>>(self) }
}
}
impl<T: PyPayload> From<PyRef<PyTuple<PyRef<T>>>> for PyTupleRef {
impl<T: PyTypedRefTarget> From<PyRef<PyTuple<PyRef<T>>>> for PyTupleRef {
#[inline]
fn from(tup: PyRef<PyTuple<PyRef<T>>>) -> Self {
tup.into_untyped()
+280 -115
View File
@@ -1,15 +1,3 @@
//! Essential types for object models
//!
//! +-------------------------+--------------+-----------------------+
//! | Management | Typed | Untyped |
//! +-------------------------+------------------+-------------------+
//! | Interpreter-independent | [`Py<T>`] | [`PyObject`] |
//! | Reference-counted | [`PyRef<T>`] | [`PyObjectRef`] |
//! | Weak | [`PyWeakRef<T>`] | [`PyRef<PyWeak>`] |
//! +-------------------------+--------------+-----------------------+
//!
//! [`PyRef<PyWeak>`] may looking like to be called as PyObjectWeak by the rule,
//! but not to do to remember it is a PyRef object.
use super::{
PyAtomicRef,
ext::{AsObject, PyRefExact, PyResult},
@@ -708,7 +696,7 @@ unsafe fn try_reuse_weakref(ptr: *mut Py<PyWeak>) -> Option<PyRef<PyWeak>> {
let node = unsafe { &*ptr };
node.ref_count
.safe_inc()
.then(|| unsafe { PyRef::from_raw(ptr) })
.then(|| unsafe { PyRef::<PyWeak>::from_raw(ptr) })
}
impl WeakRefList {
@@ -889,7 +877,7 @@ impl WeakRefList {
// Collect callback only if we can still acquire a strong ref.
if wr.ref_count.safe_inc() {
let wr_ref = unsafe { PyRef::from_raw(wr as *const Py<PyWeak>) };
let wr_ref = unsafe { PyRef::<PyWeak>::from_raw(wr as *const Py<PyWeak>) };
let cb = unsafe { wr.payload.callback.get().replace(None) };
if let Some(cb) = cb {
callbacks.push((wr_ref, cb));
@@ -940,7 +928,7 @@ impl WeakRefList {
// Collect callback without invoking only if we can keep weakref alive.
if wr.ref_count.safe_inc() {
let wr_ref = unsafe { PyRef::from_raw(wr as *const Py<PyWeak>) };
let wr_ref = unsafe { PyRef::<PyWeak>::from_raw(wr as *const Py<PyWeak>) };
let cb = unsafe { wr.payload.callback.get().replace(None) };
if let Some(cb) = cb {
callbacks.push((wr_ref, cb));
@@ -974,7 +962,7 @@ impl WeakRefList {
while let Some(node) = current {
let wr = unsafe { node.as_ref() };
if wr.ref_count.safe_inc() {
v.push(unsafe { PyRef::from_raw(wr as *const Py<PyWeak>) });
v.push(unsafe { PyRef::<PyWeak>::from_raw(wr as *const Py<PyWeak>) });
}
current = unsafe { WeakLink::pointers(node).as_ref().get_next() };
}
@@ -1001,7 +989,7 @@ unsafe impl Link for WeakLink {
#[inline(always)]
unsafe fn from_raw(ptr: NonNull<Self::Target>) -> Self::Handle {
unsafe { PyRef::from_raw(ptr.as_ptr()) }
unsafe { PyRef::<PyWeak>::from_raw(ptr.as_ptr()) }
}
#[inline(always)]
@@ -1513,38 +1501,11 @@ impl<T: PyPayload> core::ops::DerefMut for FreeList<T> {
/// this reference counting is accounted for by this type. Use the `.clone()`
/// method to create a new reference and increment the amount of references
/// to the python object by 1.
#[repr(transparent)]
pub struct PyObjectRef {
ptr: NonNull<PyObject>,
}
impl Clone for PyObjectRef {
#[inline(always)]
fn clone(&self) -> Self {
(**self).to_owned()
}
}
cfg_select! {
feature = "threading" => {
unsafe impl Send for PyObjectRef {}
unsafe impl Sync for PyObjectRef {}
}
_ => {}
}
pub type PyObjectRef = PyRef<PyObject>;
#[repr(transparent)]
pub struct PyObject(Py<Erased>);
impl Deref for PyObjectRef {
type Target = PyObject;
#[inline(always)]
fn deref(&self) -> &PyObject {
unsafe { self.ptr.as_ref() }
}
}
impl ToOwned for PyObject {
type Owned = PyObjectRef;
@@ -1552,7 +1513,8 @@ impl ToOwned for PyObject {
fn to_owned(&self) -> Self::Owned {
self.0.ref_count.inc();
PyObjectRef {
ptr: NonNull::from(self),
ptr: NonNull::from(self).cast(),
marker: PhantomData,
}
}
}
@@ -1566,7 +1528,8 @@ impl PyObject {
pub fn try_to_owned(&self) -> Option<PyObjectRef> {
if self.0.ref_count.safe_inc() {
Some(PyObjectRef {
ptr: NonNull::from(self),
ptr: NonNull::from(self).cast(),
marker: PhantomData,
})
} else {
None
@@ -1590,6 +1553,7 @@ impl PyObject {
if ref_count.safe_inc() {
Some(PyObjectRef {
ptr: unsafe { NonNull::new_unchecked(ptr) },
marker: PhantomData,
})
} else {
None
@@ -1621,7 +1585,10 @@ impl PyObjectRef {
#[inline(always)]
#[must_use]
pub const unsafe fn from_raw(ptr: NonNull<PyObject>) -> Self {
Self { ptr }
Self {
ptr: ptr.cast(),
marker: PhantomData,
}
}
/// Attempt to downcast this reference to a subclass.
@@ -1653,7 +1620,8 @@ impl PyObjectRef {
// manual impl to avoid assertion
let obj = ManuallyDrop::new(self);
PyRef {
ptr: obj.ptr.cast(),
ptr: obj.ptr,
marker: PhantomData,
}
}
@@ -1698,7 +1666,7 @@ impl PyObject {
} else {
let head = unsafe { &*head_ptr };
if head.ref_count.safe_inc() {
Some(unsafe { PyRef::from_raw(head_ptr) }.into())
Some(unsafe { PyRef::<PyWeak>::from_raw(head_ptr) }.into())
} else {
None
}
@@ -2400,20 +2368,6 @@ impl PyObject {
}
}
impl Borrow<PyObject> for PyObjectRef {
#[inline(always)]
fn borrow(&self) -> &PyObject {
self
}
}
impl AsRef<PyObject> for PyObjectRef {
#[inline(always)]
fn as_ref(&self) -> &PyObject {
self
}
}
impl<'a, T: PyPayload> From<&'a Py<T>> for &'a PyObject {
#[inline(always)]
fn from(py_ref: &'a Py<T>) -> Self {
@@ -2421,15 +2375,6 @@ impl<'a, T: PyPayload> From<&'a Py<T>> for &'a PyObject {
}
}
impl Drop for PyObjectRef {
#[inline]
fn drop(&mut self) {
if self.0.ref_count.dec() {
unsafe { PyObject::drop_slow(self.ptr) }
}
}
}
impl fmt::Debug for PyObject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// SAFETY: the vtable contains functions that accept payload types that always match up
@@ -2438,12 +2383,6 @@ impl fmt::Debug for PyObject {
}
}
impl fmt::Debug for PyObjectRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_object().fmt(f)
}
}
const STACKREF_BORROW_TAG: usize = 1;
/// A tagged stack reference to a Python object.
@@ -2655,14 +2594,15 @@ impl<T: PyPayload> Py<T> {
}
}
impl<T> ToOwned for Py<T> {
impl<T: PyRefTarget<Target = Self>> ToOwned for Py<T> {
type Owned = PyRef<T>;
#[inline(always)]
fn to_owned(&self) -> Self::Owned {
self.ref_count.inc();
PyRef {
ptr: NonNull::from(self),
ptr: NonNull::from(self).cast(),
marker: PhantomData,
}
}
}
@@ -2721,6 +2661,63 @@ impl<T: PyPayload + core::fmt::Debug> fmt::Debug for Py<T> {
}
}
mod ref_target_sealed {
pub trait Sealed {}
impl<T: super::PyPayload> Sealed for T {}
impl Sealed for super::PyObject {}
impl Sealed for crate::builtins::set::PyAnySet {}
impl<T: super::PyTypedRefTarget> Sealed for super::PyTuple<super::PyRef<T>> {}
}
/// Selects the borrowed view of a Python reference.
///
/// Payloads borrow `Py<T>`; `PyObject` borrows the erased object itself.
/// This trait is sealed so safe code cannot choose an unrelated layout.
/// Typed tuples are immutable views of the ordinary tuple allocation.
/// Internal union views borrow an erased header and dispatch on the payload;
/// they must not assume that their alternative payloads have the same layout.
///
/// An arbitrary borrowed layout cannot be registered by downstream code:
///
/// ```compile_fail
/// use rustpython_vm::object::PyRefTarget;
/// struct Unrelated;
/// impl PyRefTarget for Unrelated {
/// type Target = u8;
/// }
/// ```
pub trait PyRefTarget: ref_target_sealed::Sealed {
type Target;
}
impl<T: PyPayload> PyRefTarget for T {
type Target = Py<T>;
}
impl PyRefTarget for PyObject {
type Target = Self;
}
/// A non-erased reference target that can be an element of a typed tuple view.
///
/// This is sealed through [`PyRefTarget`]. Payloads, registered union views and
/// recursively typed tuples qualify; `PyObject` does not, since ordinary tuples
/// already implement [`PyPayload`]. This marker selects a layout, not a runtime
/// type check: checked element conversion still requires
/// [`crate::convert::TransmuteFromObject`].
pub trait PyTypedRefTarget: PyRefTarget {}
impl<T: PyRefTarget<Target = Py<T>>> PyTypedRefTarget for T {}
impl PyTypedRefTarget for crate::builtins::set::PyAnySet {}
impl PyRefTarget for crate::builtins::set::PyAnySet {
type Target = Self;
}
impl<T: PyTypedRefTarget> PyRefTarget for PyTuple<PyRef<T>> {
type Target = Py<Self>;
}
/// A reference to a Python object.
///
/// Note that a `PyRef<T>` can only deref to a shared / immutable reference.
@@ -2732,7 +2729,9 @@ impl<T: PyPayload + core::fmt::Debug> fmt::Debug for Py<T> {
/// where a reference to the same object must be returned.
#[repr(transparent)]
pub struct PyRef<T> {
ptr: NonNull<Py<T>>,
ptr: NonNull<PyObject>,
// Preserve the variance and auto traits of the former typed pointer.
marker: PhantomData<NonNull<Py<T>>>,
}
cfg_select! {
@@ -2743,17 +2742,17 @@ cfg_select! {
_ => {}
}
impl<T: fmt::Debug> fmt::Debug for PyRef<T> {
impl<T> fmt::Debug for PyRef<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(**self).fmt(f)
self.as_ref().fmt(f)
}
}
impl<T> Drop for PyRef<T> {
#[inline]
fn drop(&mut self) {
if self.ref_count.dec() {
unsafe { PyObject::drop_slow(self.ptr.cast::<PyObject>()) }
if unsafe { self.ptr.as_ref() }.0.ref_count.dec() {
unsafe { PyObject::drop_slow(self.ptr) }
}
}
}
@@ -2761,14 +2760,48 @@ impl<T> Drop for PyRef<T> {
impl<T> Clone for PyRef<T> {
#[inline(always)]
fn clone(&self) -> Self {
(**self).to_owned()
unsafe { self.ptr.as_ref() }.0.ref_count.inc();
Self {
ptr: self.ptr,
marker: PhantomData,
}
}
}
impl<T: PyRefTarget> PyRef<T> {
/// Transfers ownership from an erased reference to a registered view.
///
/// # Safety
/// The object must satisfy all invariants of `T::Target`, including its
/// payload restrictions. Registering a target establishes its layout only;
/// the caller must validate this particular object before the transfer.
#[inline]
pub(crate) unsafe fn from_object_unchecked(object: PyObjectRef) -> Self {
let object = ManuallyDrop::new(object);
Self {
ptr: object.ptr,
marker: PhantomData,
}
}
}
impl<T> PyRef<T> {
/// Erases the borrowed view, transferring the existing strong reference.
#[inline]
#[must_use]
pub fn into_object(self) -> PyObjectRef {
let object = ManuallyDrop::new(self);
PyObjectRef {
ptr: object.ptr,
marker: PhantomData,
}
}
}
impl<T: PyPayload> PyRef<T> {
#[inline(always)]
pub(super) const fn into_non_null(self) -> NonNull<Py<T>> {
let ptr = self.ptr;
let ptr = self.ptr.cast();
core::mem::forget(self);
ptr
}
@@ -2778,7 +2811,10 @@ impl<T: PyPayload> PyRef<T> {
#[must_use]
#[inline(always)]
pub const unsafe fn from_non_null(ptr: NonNull<Py<T>>) -> Self {
Self { ptr }
Self {
ptr: ptr.cast(),
marker: PhantomData,
}
}
/// # Safety
@@ -2792,15 +2828,13 @@ impl<T: PyPayload> PyRef<T> {
#[inline(always)]
unsafe fn from_obj_unchecked(obj: PyObjectRef) -> Self {
debug_assert!(obj.downcast_ref::<T>().is_some());
let obj = ManuallyDrop::new(obj);
Self {
ptr: obj.ptr.cast(),
}
// SAFETY: the caller guarantees the payload type.
unsafe { Self::from_object_unchecked(obj) }
}
#[must_use]
pub const fn leak(pyref: Self) -> &'static Py<T> {
let ptr = pyref.ptr;
let ptr = pyref.ptr.cast::<Py<T>>();
core::mem::forget(pyref);
unsafe { ptr.as_ref() }
}
@@ -2858,7 +2892,10 @@ impl<T: PyPayload + crate::object::MaybeTraverse + core::fmt::Debug> PyRef<T> {
}
}
Self { ptr }
Self {
ptr: ptr.cast(),
marker: PhantomData,
}
}
}
@@ -2915,54 +2952,47 @@ impl<T: crate::class::PySubclass> Py<T> {
}
}
impl<T> Borrow<PyObject> for PyRef<T>
where
T: PyPayload,
{
impl<T> Borrow<PyObject> for PyRef<T> {
#[inline(always)]
fn borrow(&self) -> &PyObject {
(**self).as_object()
unsafe { self.ptr.as_ref() }
}
}
impl<T> AsRef<PyObject> for PyRef<T>
where
T: PyPayload,
{
impl<T> AsRef<PyObject> for PyRef<T> {
#[inline(always)]
fn as_ref(&self) -> &PyObject {
self.borrow()
}
}
impl<T> From<PyRef<T>> for PyObjectRef {
impl<T: PyRefTarget<Target = Py<T>>> From<PyRef<T>> for PyObjectRef {
#[inline]
fn from(value: PyRef<T>) -> Self {
let me = ManuallyDrop::new(value);
Self { ptr: me.ptr.cast() }
value.into_object()
}
}
impl<T> Borrow<Py<T>> for PyRef<T> {
impl<T: PyRefTarget<Target = Py<T>>> Borrow<Py<T>> for PyRef<T> {
#[inline(always)]
fn borrow(&self) -> &Py<T> {
self
}
}
impl<T> AsRef<Py<T>> for PyRef<T> {
impl<T: PyRefTarget<Target = Py<T>>> AsRef<Py<T>> for PyRef<T> {
#[inline(always)]
fn as_ref(&self) -> &Py<T> {
self
}
}
impl<T> Deref for PyRef<T> {
type Target = Py<T>;
impl<T: PyRefTarget> Deref for PyRef<T> {
type Target = T::Target;
#[inline(always)]
fn deref(&self) -> &Py<T> {
unsafe { self.ptr.as_ref() }
fn deref(&self) -> &Self::Target {
unsafe { self.ptr.cast::<T::Target>().as_ref() }
}
}
@@ -3060,12 +3090,12 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy {
}
unsafe fn initial_ref<T: PyPayload>(ptr: *mut Py<T>) -> PyRef<T> {
unsafe { PyRef::from_raw(ptr.cast()) }
unsafe { PyRef::<T>::from_raw(ptr.cast()) }
}
unsafe fn clone_raw_ref<T: PyPayload>(ptr: *mut Py<T>) -> PyRef<T> {
unsafe { &*ptr::addr_of!((*ptr).ref_count) }.inc();
unsafe { PyRef::from_raw(ptr.cast()) }
unsafe { PyRef::<T>::from_raw(ptr.cast()) }
}
unsafe fn into_type_tuple(tuple: PyTupleRef) -> PyTypeTupleRef {
@@ -3245,6 +3275,141 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy {
mod tests {
use super::*;
#[derive(Debug)]
struct RefProbe(alloc::sync::Arc<core::sync::atomic::AtomicUsize>);
impl MaybeTraverse for RefProbe {
fn try_traverse(&self, _traverse_fn: &mut TraverseFn<'_>) {}
}
impl PyPayload for RefProbe {
fn class(ctx: &crate::Context) -> &'static Py<PyType> {
ctx.types.object_type
}
}
impl Drop for RefProbe {
fn drop(&mut self) {
self.0.fetch_add(1, core::sync::atomic::Ordering::SeqCst);
}
}
#[test]
fn pyref_object_layout_and_views() {
static_assertions::assert_type_eq_all!(PyObjectRef, PyRef<PyObject>);
static_assertions::assert_not_impl_any!(PyObject: PyPayload);
static_assertions::assert_impl_all!(PyRef<core::marker::PhantomPinned>: Unpin);
static_assertions::assert_eq_size!(
PyObjectRef,
NonNull<PyObject>,
Option<PyObjectRef>,
PyRef<RefProbe>,
Option<PyRef<RefProbe>>,
PyRef<PyTuple<PyRef<RefProbe>>>
);
static_assertions::assert_eq_align!(
PyObjectRef,
NonNull<PyObject>,
PyRef<RefProbe>,
PyRef<PyTuple<PyRef<RefProbe>>>
);
#[cfg(feature = "threading")]
static_assertions::assert_impl_all!(PyRef<PyObject>: Send, Sync);
#[cfg(not(feature = "threading"))]
static_assertions::assert_not_impl_any!(PyRef<PyObject>: Send, Sync);
fn erased_view(reference: &PyRef<PyObject>) -> &PyObject {
reference
}
fn payload_view(reference: &PyRef<RefProbe>) -> &Py<RefProbe> {
reference
}
fn tuple_view(
reference: &PyRef<PyTuple<PyRef<RefProbe>>>,
) -> &Py<PyTuple<PyRef<RefProbe>>> {
reference
}
type NestedTuple = PyTuple<PyRef<PyTuple<PyRef<RefProbe>>>>;
fn nested_view(reference: &PyRef<NestedTuple>) -> &Py<NestedTuple> {
reference
}
fn covariance<'long: 'short, 'short>(reference: PyRef<&'long ()>) -> PyRef<&'short ()> {
reference
}
fn into_object<T: Into<PyObjectRef>>() {}
into_object::<PyRef<NestedTuple>>();
let _ = (
erased_view,
payload_view,
tuple_view,
nested_view,
covariance,
);
}
#[test]
fn pyref_object_ownership_and_raw_roundtrip() {
let drops = alloc::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
let ctx = crate::Context::genesis();
let typed = PyRef::new_ref(
RefProbe(drops.clone()),
ctx.types.object_type.to_owned(),
None,
);
let pointer = typed.as_object() as *const PyObject;
assert_eq!(typed.as_object().strong_count(), 1);
let erased: PyRef<PyObject> = typed.clone().into();
assert_eq!(erased.strong_count(), 2);
assert!(core::ptr::eq(&*erased, pointer));
let raw = erased.into_raw();
assert_eq!(typed.as_object().strong_count(), 2);
let erased = unsafe { PyRef::<PyObject>::from_raw(raw) };
let erased = erased.downcast::<crate::builtins::PyBytes>().unwrap_err();
assert_eq!(erased.strong_count(), 2);
let restored = erased.downcast::<RefProbe>().unwrap();
assert!(core::ptr::eq(&*typed, &*restored));
let raw = restored.into_non_null();
let restored = unsafe { PyRef::<RefProbe>::from_non_null(raw) };
drop(typed);
assert_eq!(restored.as_object().strong_count(), 1);
assert_eq!(drops.load(core::sync::atomic::Ordering::SeqCst), 0);
drop(restored);
assert_eq!(drops.load(core::sync::atomic::Ordering::SeqCst), 1);
}
#[test]
fn pyref_typed_tuple_erased_storage_drops_once() {
let ctx = crate::Context::genesis();
// Both constructors transfer a Vec allocation into erased tuple storage.
for explicit_type in [false, true] {
let drops = alloc::sync::Arc::new(core::sync::atomic::AtomicUsize::new(0));
let item = PyRef::new_ref(
RefProbe(drops.clone()),
ctx.types.object_type.to_owned(),
None,
);
let mut elements = Vec::with_capacity(4);
elements.push(item.clone());
let tuple = if explicit_type {
PyTuple::new_ref_typed_with_type(elements, ctx.types.tuple_type.to_owned())
} else {
PyTuple::new_ref_typed(elements, ctx)
};
assert_eq!(tuple.as_slice()[0].as_object().strong_count(), 2);
let copy = (*tuple).to_owned();
let object: PyObjectRef = tuple.into();
let untyped = copy.into_untyped();
assert_eq!(object.strong_count(), 2);
assert!(core::ptr::eq(untyped.as_object(), &*object));
assert_eq!(untyped.as_slice()[0].strong_count(), 2);
drop(object);
drop(item);
assert_eq!(drops.load(core::sync::atomic::Ordering::SeqCst), 0);
drop(untyped);
assert_eq!(drops.load(core::sync::atomic::Ordering::SeqCst), 1);
}
}
#[test]
fn native_type_basicsize_includes_payload_padding() {
use crate::class::PyClassDef;
+6 -2
View File
@@ -360,7 +360,7 @@ impl<T: PyPayload> PyAtomicRef<T> {
pub unsafe fn swap(&self, pyref: PyRef<T>) -> PyRef<T> {
let py = PyRef::leak(pyref) as *const Py<T> as *mut _;
let old = Radium::swap(&self.inner, py, Ordering::AcqRel);
unsafe { PyRef::from_raw(old.cast()) }
unsafe { PyRef::<T>::from_raw(old.cast()) }
}
pub fn swap_to_temporary_refs(&self, pyref: PyRef<T>, vm: &VirtualMachine) {
@@ -439,7 +439,11 @@ impl<T: PyPayload> PyAtomicRef<Option<T>> {
pub unsafe fn swap(&self, opt_ref: Option<PyRef<T>>) -> Option<PyRef<T>> {
let val = opt_ref.map_or(null_mut(), |x| PyRef::leak(x) as *const Py<T> as *mut _);
let old = Radium::swap(&self.inner, val, Ordering::AcqRel);
unsafe { old.cast::<Py<T>>().as_ref().map(|x| PyRef::from_raw(x)) }
unsafe {
old.cast::<Py<T>>()
.as_ref()
.map(|x| PyRef::<T>::from_raw(x))
}
}
pub fn swap_to_temporary_refs(&self, opt_ref: Option<PyRef<T>>, vm: &VirtualMachine) {
+75
View File
@@ -1,3 +1,78 @@
//! Essential types for object models
//!
//! +-------------------------+--------------+-----------------------+
//! | Management | Typed | Untyped |
//! +-------------------------+------------------+-------------------+
//! | Interpreter-independent | [`Py<T>`] | [`PyObject`] |
//! | Reference-counted | [`PyRef<T>`] | [`PyObjectRef`] |
//! | Weak | [`PyWeakRef<T>`] | [`PyRef<PyWeak>`] |
//! +-------------------------+--------------+-----------------------+
//!
//! [`PyRef<PyWeak>`] may looking like to be called as PyObjectWeak by the rule,
//! but not to do to remember it is a PyRef object.
//!
//! # Reference ownership
//!
//! `PyObjectRef` is an alias for `PyRef<PyObject>`. Typed payload references,
//! erased references, union views, and typed tuple views share the same owning representation.
//!
//! ## Representation and invariants
//!
//! - `PyRef<T>` stores `NonNull<PyObject>` and a zero-sized
//! `PhantomData<NonNull<Py<T>>>`. The latter preserves the previous typed pointer's
//! covariance and auto-trait/`Unpin` shape. The transparent owner and its `Option`
//! remain one pointer wide with pointer alignment.
//! - The sealed `object::PyRefTarget` selects `Deref::Target`: payload `T` maps to
//! `Py<T>`, `PyObject` maps to itself, and immutable typed tuples map recursively
//! to their existing typed `Py` view. Downstream safe code cannot register an
//! unrelated layout. `PyObject` does not implement `PyPayload`.
//! - `PyAnySet` is an internal transparent `PyObject` view. Its owner is
//! `PyRef<PyAnySet>`, whose construction validates set/frozenset (including
//! subclasses). Payload access downcasts to the actual alternative; neither
//! `Py<PyAnySet>` nor a shared set/frozenset payload layout is assumed.
//! `ArgIterable` instead stores a cached iterator function beside its owner,
//! so it remains a wrapper rather than an allocation view.
//! - [`PyTypedRefTarget`] admits non-erased element targets for typed tuple
//! views, including union views and recursively typed tuples. It does not
//! perform validation: `try_into_typed` checks every element before changing
//! the view. Typed constructors instead receive already-typed owners.
//! All tuple views retain erased element storage with transparent wrappers;
//! typed slices reinterpret only the guaranteed transparent reference
//! elements, never a generic `Vec` or `Box` representation. Private marshal
//! initialization finishes before publication; published elements are immutable.
//! - The owner itself has no new generic bound. `Clone` and `Drop` share one
//! implementation over the existing object header. Object allocation, vtable
//! destruction, weakrefs, GC, and trashcan handling are unchanged.
//! - Typed erasure uses `T: PyRefTarget<Target = Py<T>>`. This excludes the erased
//! type from the conversion blanket, avoiding overlap with Rust's identity
//! `From<T> for T`, while retaining typed and nested tuple conversions.
//! Union views provide their own checked conversions; `PyRef::into_object`
//! erases any owning view without adjusting its strong count.
//! - Ownership transfers use `ManuallyDrop` or `forget`, without an extra incref or
//! decrement. The existing raw APIs retain their ownership-transfer contracts.
//! - C-API typed output uses the same target equality. An erased owner supports
//! `*mut PyObject`, but cannot produce the fictitious `*mut Py<PyObject>` output.
//! Duplicate erased owner/member-layout/FFI implementations are removed.
//! - Feature-dependent `Send`/`Sync` behavior is retained. The non-threading erased
//! owner is neither; the threading owner implements both.
//!
//! ## API compatibility considerations
//!
//! Existing payload `PyRef<T>` and `PyObjectRef` use remains supported, but aliasing
//! makes some inference less direct:
//!
//! - Distinct raw constructor signatures remain: erased
//! `PyObjectRef::from_raw(NonNull<PyObject>)` and typed
//! `PyRef::<T>::from_raw(*const Py<T>)`. Unqualified `PyRef::from_raw` can now be
//! ambiguous and require the explicit type parameter.
//! - Associated-target dereferencing can require an explicit downcast type or a
//! `PyRefTarget` equality bound in generic typed-view code. Arbitrary non-payload
//! views no longer automatically gain typed dereferencing; supported internal
//! views must be registered in the sealed selector.
//! - Rust `Debug` formatting now consistently uses the allocation's vtable rather
//! than the statically selected payload's formatter. This is a Rust diagnostic
//! difference, not a Python semantic assertion.
mod core;
mod ext;
mod payload;