Skip to content

Commit 14e0139

Browse files
committed
enable extending native types on abi3
1 parent cc0e690 commit 14e0139

12 files changed

Lines changed: 59 additions & 85 deletions

File tree

guide/src/class.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ To convert between the Rust type and its native base class, you can take `slf` a
528528
To access the Rust fields use `slf.borrow()` or `slf.borrow_mut()`, and to access the base class use `slf.cast::<BaseClass>()`.
529529

530530
```rust
531-
# #[cfg(not(Py_LIMITED_API))] {
531+
# #[cfg(any(not(Py_LIMITED_API), Py_3_12))] {
532532
# use pyo3::prelude::*;
533533
use pyo3::types::PyDict;
534534
use std::collections::HashMap;
@@ -588,7 +588,7 @@ Be sure to accept arguments in the `#[new]` method that you want the base class
588588

589589
```rust
590590
# #[allow(dead_code)]
591-
# #[cfg(not(Py_LIMITED_API))] {
591+
# #[cfg(any(not(Py_LIMITED_API), Py_3_12))] {
592592
# use pyo3::prelude::*;
593593
use pyo3::types::PyDict;
594594

guide/src/exception.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ If you need to create an exception with more complex behavior, you can also manu
133133

134134
```rust
135135
#![allow(dead_code)]
136-
# #[cfg(any(not(feature = "abi3")))] {
136+
# #[cfg(any(not(Py_LIMITED_API), Py_3_12))] {
137137
use pyo3::prelude::*;
138138
use pyo3::types::IntoPyDict;
139139
use pyo3::exceptions::PyException;

newsfragments/5733.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
added support for subclassing native types (`PyDict`, exceptions, ...) when building for abi3 on Python 3.12+

src/impl_/pyclass_init.rs

Lines changed: 16 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
//! Contains initialization utilities for `#[pyclass]`.
2+
use crate::exceptions::PyTypeError;
23
use crate::ffi_ptr_ext::FfiPtrExt;
3-
use crate::internal::get_slot::TP_ALLOC;
4-
use crate::types::PyType;
5-
use crate::{ffi, Borrowed, PyErr, PyResult, Python};
4+
use crate::internal::get_slot::TP_NEW;
5+
use crate::types::{PyTuple, PyType};
6+
use crate::{ffi, PyErr, PyResult, Python};
67
use crate::{ffi::PyTypeObject, sealed::Sealed, type_object::PyTypeInfo};
78
use std::marker::PhantomData;
8-
use std::ptr;
99

1010
/// Initializer for Python types.
1111
///
@@ -35,55 +35,26 @@ impl<T: PyTypeInfo> PyObjectInit<T> for PyNativeTypeInitializer<T> {
3535
) -> PyResult<*mut ffi::PyObject> {
3636
unsafe fn inner(
3737
py: Python<'_>,
38-
type_object: *mut PyTypeObject,
38+
type_ptr: *mut PyTypeObject,
3939
subtype: *mut PyTypeObject,
4040
) -> PyResult<*mut ffi::PyObject> {
41-
// HACK (due to FIXME below): PyBaseObject_Type's tp_new isn't happy with NULL arguments
42-
let is_base_object = ptr::eq(type_object, ptr::addr_of!(ffi::PyBaseObject_Type));
43-
let subtype_borrowed: Borrowed<'_, '_, PyType> = unsafe {
44-
subtype
41+
let tp_new = unsafe {
42+
type_ptr
4543
.cast::<ffi::PyObject>()
4644
.assume_borrowed_unchecked(py)
47-
.cast_unchecked()
45+
.cast_unchecked::<PyType>()
46+
.get_slot(TP_NEW)
47+
.ok_or_else(|| PyTypeError::new_err("base type without tp_new"))?
4848
};
4949

50-
if is_base_object {
51-
let alloc = subtype_borrowed
52-
.get_slot(TP_ALLOC)
53-
.unwrap_or(ffi::PyType_GenericAlloc);
54-
55-
let obj = unsafe { alloc(subtype, 0) };
56-
return if obj.is_null() {
57-
Err(PyErr::fetch(py))
58-
} else {
59-
Ok(obj)
60-
};
61-
}
62-
63-
#[cfg(Py_LIMITED_API)]
64-
unreachable!("subclassing native types is not possible with the `abi3` feature");
65-
66-
#[cfg(not(Py_LIMITED_API))]
67-
{
68-
match unsafe { (*type_object).tp_new } {
69-
// FIXME: Call __new__ with actual arguments
70-
Some(newfunc) => {
71-
let obj =
72-
unsafe { newfunc(subtype, std::ptr::null_mut(), std::ptr::null_mut()) };
73-
if obj.is_null() {
74-
Err(PyErr::fetch(py))
75-
} else {
76-
Ok(obj)
77-
}
78-
}
79-
None => Err(crate::exceptions::PyTypeError::new_err(
80-
"base type without tp_new",
81-
)),
82-
}
50+
let obj = unsafe { tp_new(subtype, PyTuple::empty(py).as_ptr(), std::ptr::null_mut()) };
51+
if obj.is_null() {
52+
Err(PyErr::fetch(py))
53+
} else {
54+
Ok(obj)
8355
}
8456
}
85-
let type_object = T::type_object_raw(py);
86-
unsafe { inner(py, type_object, subtype) }
57+
unsafe { inner(py, T::type_object_raw(py), subtype) }
8758
}
8859

8960
#[inline]

src/internal/get_slot.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ macro_rules! impl_slots {
124124

125125
// Slots are implemented on-demand as needed.)
126126
impl_slots! {
127-
TP_ALLOC: (Py_tp_alloc, tp_alloc) -> Option<ffi::allocfunc>,
127+
TP_NEW: (Py_tp_new, tp_new) -> Option<ffi::newfunc>,
128+
TP_DEALLOC: (Py_tp_dealloc, tp_dealloc) -> Option<ffi::destructor>,
128129
TP_BASE: (Py_tp_base, tp_base) -> *mut ffi::PyTypeObject,
129130
TP_CLEAR: (Py_tp_clear, tp_clear) -> Option<ffi::inquiry>,
130131
TP_DESCR_GET: (Py_tp_descr_get, tp_descr_get) -> Option<ffi::descrgetfunc>,

src/pycell/impl_.rs

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
1010
use crate::impl_::pyclass::{
1111
PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, PyObjectOffset,
1212
};
13-
use crate::internal::get_slot::TP_FREE;
13+
use crate::internal::get_slot::{TP_DEALLOC, TP_FREE};
1414
use crate::type_object::{PyLayout, PySizedLayout};
1515
use crate::types::PyType;
1616
use crate::{ffi, PyClass, PyTypeInfo, Python};
@@ -273,27 +273,19 @@ unsafe fn tp_dealloc(slf: *mut ffi::PyObject, type_obj: &crate::Bound<'_, PyType
273273
}
274274

275275
// More complex native types (e.g. `extends=PyDict`) require calling the base's dealloc.
276-
#[cfg(not(Py_LIMITED_API))]
277-
{
278-
// FIXME: should this be using actual_type.tp_dealloc?
279-
if let Some(dealloc) = (*type_ptr).tp_dealloc {
280-
// Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which
281-
// assumes the exception is currently GC tracked, so we have to re-track
282-
// before calling the dealloc so that it can safely call Py_GC_UNTRACK.
283-
#[cfg(not(any(Py_3_11, PyPy)))]
284-
if ffi::PyType_FastSubclass(type_ptr, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 {
285-
ffi::PyObject_GC_Track(slf.cast());
286-
}
287-
dealloc(slf);
288-
} else {
289-
(*actual_type.as_type_ptr())
290-
.tp_free
291-
.expect("type missing tp_free")(slf.cast());
276+
// FIXME: should this be using actual_type.tp_dealloc?
277+
if let Some(dealloc) = type_obj.get_slot(TP_DEALLOC) {
278+
// Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which
279+
// assumes the exception is currently GC tracked, so we have to re-track
280+
// before calling the dealloc so that it can safely call Py_GC_UNTRACK.
281+
#[cfg(not(any(Py_3_11, PyPy)))]
282+
if ffi::PyType_FastSubclass(type_ptr, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 {
283+
ffi::PyObject_GC_Track(slf.cast());
292284
}
285+
dealloc(slf);
286+
} else {
287+
type_obj.get_slot(TP_FREE).expect("type missing tp_free")(slf.cast());
293288
}
294-
295-
#[cfg(Py_LIMITED_API)]
296-
unreachable!("subclassing native types is not possible with the `abi3` feature");
297289
}
298290
}
299291

src/tests/hygiene/pymethods.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -458,11 +458,11 @@ struct WarningDummy {
458458
value: i32,
459459
}
460460

461-
#[cfg(not(Py_LIMITED_API))]
461+
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
462462
#[crate::pyclass(crate = "crate", extends=crate::exceptions::PyWarning)]
463463
pub struct UserDefinedWarning {}
464464

465-
#[cfg(not(Py_LIMITED_API))]
465+
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
466466
#[crate::pymethods(crate = "crate")]
467467
impl UserDefinedWarning {
468468
#[new]
@@ -489,7 +489,7 @@ impl WarningDummy {
489489
#[pyo3(warn(message = "this method raises warning", category = crate::exceptions::PyFutureWarning))]
490490
fn method_with_warning_and_custom_category(_slf: crate::PyRef<'_, Self>) {}
491491

492-
#[cfg(not(Py_LIMITED_API))]
492+
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
493493
#[pyo3(warn(message = "this method raises user-defined warning", category = UserDefinedWarning))]
494494
fn method_with_warning_and_user_defined_category(&self) {}
495495

@@ -561,7 +561,7 @@ impl WarningDummy2 {
561561
#[pyo3(warn(message = "this class-method raises future warning", category = crate::exceptions::PyFutureWarning))]
562562
fn multiple_warnings_fn(&self) {}
563563

564-
#[cfg(not(Py_LIMITED_API))]
564+
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
565565
#[pyo3(warn(message = "this class-method raises future warning", category = crate::exceptions::PyFutureWarning))]
566566
#[pyo3(warn(message = "this class-method raises user-defined warning", category = UserDefinedWarning))]
567567
fn multiple_warnings_fn_with_custom_category(&self) {}

src/types/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,15 @@ macro_rules! pyobject_subclassable_native_type {
228228
type PyClassMutability = $crate::pycell::impl_::ImmutableClass;
229229
type Layout<T: $crate::impl_::pyclass::PyClassImpl> = $crate::impl_::pycell::PyStaticClassObject<T>;
230230
}
231+
232+
#[cfg(all(Py_3_12, Py_LIMITED_API))]
233+
impl<$($generics,)*> $crate::impl_::pyclass::PyClassBaseType for $name {
234+
type LayoutAsBase = $crate::impl_::pycell::PyVariableClassObjectBase;
235+
type BaseNativeType = Self;
236+
type Initializer = $crate::impl_::pyclass_init::PyNativeTypeInitializer<Self>;
237+
type PyClassMutability = $crate::pycell::impl_::ImmutableClass;
238+
type Layout<T: $crate::impl_::pyclass::PyClassImpl> = $crate::impl_::pycell::PyVariableClassObject<T>;
239+
}
231240
}
232241
}
233242

tests/test_compile_error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ fn test_compile_errors() {
4949
t.compile_fail("tests/ui/invalid_pyfunctions.rs");
5050
t.compile_fail("tests/ui/invalid_pymethods.rs");
5151
// output changes with async feature
52-
#[cfg(all(Py_LIMITED_API, feature = "experimental-async"))]
52+
#[cfg(all(not(Py_3_12), Py_LIMITED_API, feature = "experimental-async"))]
5353
t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs");
5454
#[cfg(not(feature = "experimental-async"))]
5555
t.compile_fail("tests/ui/invalid_async.rs");

tests/test_inheritance.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ except Exception as e:
173173
}
174174

175175
// Subclassing builtin types is not allowed in the LIMITED API.
176-
#[cfg(not(Py_LIMITED_API))]
176+
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
177177
mod inheriting_native_type {
178178
use super::*;
179179
use pyo3::exceptions::PyException;

0 commit comments

Comments
 (0)