pyo3/pyclass_init.rs
1// TODO https://github.com/PyO3/pyo3/issues/5487
2#![allow(clippy::undocumented_unsafe_blocks)]
3
4//! Contains initialization utilities for `#[pyclass]`.
5use crate::ffi_ptr_ext::FfiPtrExt;
6use crate::impl_::pyclass::{PyClassBaseType, PyClassImpl};
7use crate::impl_::pyclass_init::PyNativeTypeInitializer;
8use crate::internal::pyclass_init::PyObjectInit;
9use crate::pycell::impl_::PyClassObjectLayout;
10use crate::{ffi, Bound, PyClass, PyResult, Python};
11use crate::{ffi::PyTypeObject, pycell::impl_::PyClassObjectContents};
12use core::marker::PhantomData;
13
14/// Initializer for our `#[pyclass]` system.
15///
16/// You can use this type to initialize complicatedly nested `#[pyclass]`.
17///
18/// # Examples
19///
20/// ```
21/// # use pyo3::prelude::*;
22/// # use pyo3::py_run;
23/// #[pyclass(subclass)]
24/// struct BaseClass {
25/// #[pyo3(get)]
26/// basename: &'static str,
27/// }
28/// #[pyclass(extends=BaseClass, subclass)]
29/// struct SubClass {
30/// #[pyo3(get)]
31/// subname: &'static str,
32/// }
33/// #[pyclass(extends=SubClass)]
34/// struct SubSubClass {
35/// #[pyo3(get)]
36/// subsubname: &'static str,
37/// }
38///
39/// #[pymethods]
40/// impl SubSubClass {
41/// #[new]
42/// fn new() -> PyClassInitializer<Self> {
43/// PyClassInitializer::from(BaseClass { basename: "base" })
44/// .add_subclass(SubClass { subname: "sub" })
45/// .add_subclass(SubSubClass {
46/// subsubname: "subsub",
47/// })
48/// }
49/// }
50/// Python::attach(|py| {
51/// let typeobj = py.get_type::<SubSubClass>();
52/// let sub_sub_class = typeobj.call((), None).unwrap();
53/// py_run!(
54/// py,
55/// sub_sub_class,
56/// r#"
57/// assert sub_sub_class.basename == 'base'
58/// assert sub_sub_class.subname == 'sub'
59/// assert sub_sub_class.subsubname == 'subsub'"#
60/// );
61/// });
62/// ```
63pub struct PyClassInitializer<T: PyClass> {
64 init: T,
65 super_init: <T::BaseType as PyClassBaseType>::Initializer,
66}
67
68impl<T: PyClass> PyClassInitializer<T> {
69 /// Constructs a new initializer from value `T` and base class' initializer.
70 ///
71 /// It is recommended to use `add_subclass` instead of this method for most usage.
72 #[track_caller]
73 #[inline]
74 pub fn new(init: T, super_init: <T::BaseType as PyClassBaseType>::Initializer) -> Self {
75 Self { init, super_init }
76 }
77
78 /// Constructs a new initializer from an initializer for the base class.
79 ///
80 /// # Examples
81 /// ```
82 /// use pyo3::prelude::*;
83 ///
84 /// #[pyclass(subclass)]
85 /// struct BaseClass {
86 /// #[pyo3(get)]
87 /// value: i32,
88 /// }
89 ///
90 /// impl BaseClass {
91 /// fn new(value: i32) -> PyResult<Self> {
92 /// Ok(Self { value })
93 /// }
94 /// }
95 ///
96 /// #[pyclass(extends=BaseClass)]
97 /// struct SubClass {}
98 ///
99 /// #[pymethods]
100 /// impl SubClass {
101 /// #[new]
102 /// fn new(value: i32) -> PyResult<PyClassInitializer<Self>> {
103 /// let base_init = PyClassInitializer::from(BaseClass::new(value)?);
104 /// Ok(base_init.add_subclass(SubClass {}))
105 /// }
106 /// }
107 ///
108 /// fn main() -> PyResult<()> {
109 /// Python::attach(|py| {
110 /// let m = PyModule::new(py, "example")?;
111 /// m.add_class::<SubClass>()?;
112 /// m.add_class::<BaseClass>()?;
113 ///
114 /// let instance = m.getattr("SubClass")?.call1((92,))?;
115 ///
116 /// // `SubClass` does not have a `value` attribute, but `BaseClass` does.
117 /// let n = instance.getattr("value")?.extract::<i32>()?;
118 /// assert_eq!(n, 92);
119 ///
120 /// Ok(())
121 /// })
122 /// }
123 /// ```
124 #[track_caller]
125 #[inline]
126 pub fn add_subclass<S>(self, subclass_value: S) -> PyClassInitializer<S>
127 where
128 T: PyClassBaseType<Initializer = Self>,
129 S: PyClass<BaseType = T>,
130 {
131 PyClassInitializer::new(subclass_value, self)
132 }
133
134 /// Creates a new class object and initializes it.
135 pub(crate) fn create_class_object(self, py: Python<'_>) -> PyResult<Bound<'_, T>>
136 where
137 T: PyClass,
138 {
139 unsafe { self.create_class_object_of_type(py, T::type_object_raw(py)) }
140 }
141
142 /// Creates a new class object and initializes it given a typeobject `subtype`.
143 ///
144 /// # Safety
145 /// `subtype` must be a valid pointer to the type object of T or a subclass.
146 pub(crate) unsafe fn create_class_object_of_type(
147 self,
148 py: Python<'_>,
149 target_type: *mut crate::ffi::PyTypeObject,
150 ) -> PyResult<Bound<'_, T>>
151 where
152 T: PyClass,
153 {
154 let obj = unsafe { self.super_init.into_new_object(py, target_type)? };
155
156 // SAFETY: `obj` is constructed using `T::Layout` but has not been initialized yet
157 let contents = unsafe { <T as PyClassImpl>::Layout::contents_uninit(obj) };
158
159 let new_contents = PyClassObjectContents::new(self.init);
160
161 // CPython 3.11 and 3.12 eagerly create the instance dict for types with a nonzero
162 // `tp_dictoffset` in `_PyObject_InitializeDict`, storing an owned reference in
163 // the `__dict__` slot, which lives inside `contents`. Carry that value over
164 // instead of clobbering it below, otherwise it leaks. Python 3.13 returned to
165 // creating the instance dict lazily
166 //
167 // The condition is `not(Py_3_13)` rather than `all(Py_3_11, not(Py_3_13))`
168 // because an abi3 build with a lower minimum version can still run on 3.11 and
169 // 3.12; on 3.10 and older this is a harmless no-op (the slot is always null
170 // there).
171 #[cfg(not(Py_3_13))]
172 let new_contents = {
173 let mut new_contents = new_contents;
174 if eagerly_created_dict_possible::<T>(py) {
175 // SAFETY: `tp_alloc` zero-initializes the object, so the slot contains either
176 // zeroes (a valid empty slot value) or a valid owned pointer stored by the base
177 // `tp_new` through the type's `tp_dictoffset`.
178 unsafe {
179 let contents_ptr = (*contents).as_mut_ptr();
180 let dict_ptr = &raw const (*contents_ptr).dict;
181 new_contents.dict = core::ptr::read(dict_ptr);
182 }
183 }
184 new_contents
185 };
186
187 // SAFETY: `contents` is a non-null pointer to the space allocated for our
188 // `PyClassObjectContents` (either statically in Rust or dynamically by Python)
189 unsafe { (*contents).write(new_contents) };
190
191 // Safety: obj is a valid pointer to an object of type `target_type`, which` is a known
192 // subclass of `T`
193 Ok(unsafe { obj.assume_owned(py).cast_into_unchecked() })
194 }
195}
196
197/// Whether the running interpreter may have eagerly created an instance dict for `T`
198/// during `tp_new` (CPython 3.11 and 3.12 only).
199///
200/// For native builds the compile-time `not(Py_3_13)` gate at the call site is exact; abi3
201/// builds with a minimum version below 3.13 must check the interpreter version at runtime
202#[cfg(not(Py_3_13))]
203#[inline]
204fn eagerly_created_dict_possible<T: PyClassImpl>(py: Python<'_>) -> bool {
205 if core::mem::size_of::<T::Dict>() == 0 {
206 return false;
207 }
208 cfg_select! {
209 Py_LIMITED_API =>
210 {
211 use crate::sync::PyOnceLock;
212 static IS_PYTHON_3_11_OR_3_12: PyOnceLock<bool> = PyOnceLock::new();
213 *IS_PYTHON_3_11_OR_3_12.get_or_init(py, || {
214 let version_info = py.version_info();
215 matches!((version_info.major, version_info.minor), (3, 11) | (3, 12))
216 })
217 }
218 not(Py_LIMITED_API) =>
219 {
220 let _ = py;
221 cfg!(Py_3_11)
222 }
223 }
224}
225
226impl<T: PyClass> PyObjectInit<T> for PyClassInitializer<T> {
227 unsafe fn into_new_object(
228 self,
229 py: Python<'_>,
230 subtype: *mut PyTypeObject,
231 ) -> PyResult<*mut ffi::PyObject> {
232 unsafe {
233 self.create_class_object_of_type(py, subtype)
234 .map(Bound::into_ptr)
235 }
236 }
237}
238
239impl<T> From<T> for PyClassInitializer<T>
240where
241 T: PyClass,
242 T::BaseType: PyClassBaseType<Initializer = PyNativeTypeInitializer<T::BaseType>>,
243{
244 #[inline]
245 fn from(value: T) -> PyClassInitializer<T> {
246 Self::new(value, PyNativeTypeInitializer(PhantomData))
247 }
248}
249
250impl<S, B> From<(S, B)> for PyClassInitializer<S>
251where
252 S: PyClass<BaseType = B>,
253 B: PyClass + PyClassBaseType<Initializer = PyClassInitializer<B>>,
254 B::BaseType: PyClassBaseType<Initializer = PyNativeTypeInitializer<B::BaseType>>,
255{
256 #[track_caller]
257 #[inline]
258 fn from(sub_and_base: (S, B)) -> PyClassInitializer<S> {
259 let (sub, base) = sub_and_base;
260 PyClassInitializer::from(base).add_subclass(sub)
261 }
262}