Skip to main content

pyo3/pyclass/
create_type_object.rs

1use crate::exceptions::PyAttributeError;
2use crate::impl_::pymethods::{Deleter, PyDeleterDef};
3#[cfg(not(Py_3_10))]
4use crate::types::typeobject::PyTypeMethods;
5use crate::{
6    exceptions::PyTypeError,
7    ffi,
8    ffi_ptr_ext::FfiPtrExt,
9    impl_::{
10        pyclass::{
11            assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc,
12            tp_dealloc_with_gc, PyClassImpl, PyClassItemsIter, PyObjectOffset,
13        },
14        pymethods::{Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter, _call_clear},
15        trampoline::trampoline,
16    },
17    pycell::impl_::PyClassObjectLayout,
18    types::PyType,
19    Py, PyClass, PyResult, PyTypeInfo, Python,
20};
21use std::{
22    collections::HashMap,
23    ffi::{CStr, CString},
24    os::raw::{c_char, c_int, c_ulong, c_void},
25    ptr::{self, NonNull},
26};
27
28pub(crate) struct PyClassTypeObject {
29    pub type_object: Py<PyType>,
30    pub is_immutable_type: bool,
31    #[expect(
32        dead_code,
33        reason = "this is just storage that must live as long as the type object"
34    )]
35    getset_defs: Vec<GetSetDefType>,
36}
37
38pub(crate) fn create_type_object<T>(py: Python<'_>) -> PyResult<PyClassTypeObject>
39where
40    T: PyClass,
41{
42    // Written this way to monomorphize the majority of the logic.
43    #[expect(clippy::too_many_arguments)]
44    unsafe fn inner(
45        py: Python<'_>,
46        base: *mut ffi::PyTypeObject,
47        dealloc: unsafe extern "C" fn(*mut ffi::PyObject),
48        dealloc_with_gc: unsafe extern "C" fn(*mut ffi::PyObject),
49        is_mapping: bool,
50        is_sequence: bool,
51        is_immutable_type: bool,
52        doc: &'static CStr,
53        dict_offset: Option<PyObjectOffset>,
54        weaklist_offset: Option<PyObjectOffset>,
55        is_basetype: bool,
56        items_iter: PyClassItemsIter,
57        name: &'static str,
58        module: Option<&'static str>,
59        basicsize: ffi::Py_ssize_t,
60    ) -> PyResult<PyClassTypeObject> {
61        unsafe {
62            PyTypeBuilder {
63                slots: Vec::new(),
64                method_defs: Vec::new(),
65                member_defs: Vec::new(),
66                getset_builders: HashMap::new(),
67                #[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
68                cleanup: Vec::new(),
69                tp_base: base,
70                tp_dealloc: dealloc,
71                tp_dealloc_with_gc: dealloc_with_gc,
72                is_mapping,
73                is_sequence,
74                is_immutable_type,
75                has_new: false,
76                has_dealloc: false,
77                has_getitem: false,
78                has_setitem: false,
79                has_traverse: false,
80                has_clear: false,
81                dict_offset: None,
82                class_flags: 0,
83                #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
84                buffer_procs: Default::default(),
85            }
86            .type_doc(doc)
87            .offsets(dict_offset, weaklist_offset)
88            .set_is_basetype(is_basetype)
89            .class_items(items_iter)
90            .build(py, name, module, basicsize)
91        }
92    }
93
94    unsafe {
95        inner(
96            py,
97            T::BaseType::type_object_raw(py),
98            tp_dealloc::<T>,
99            tp_dealloc_with_gc::<T>,
100            T::IS_MAPPING,
101            T::IS_SEQUENCE,
102            T::IS_IMMUTABLE_TYPE,
103            T::DOC,
104            T::dict_offset(),
105            T::weaklist_offset(),
106            T::IS_BASETYPE,
107            T::items_iter(),
108            <T as PyClass>::NAME,
109            <T as PyClassImpl>::MODULE,
110            <T as PyClassImpl>::Layout::BASIC_SIZE,
111        )
112    }
113}
114
115#[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
116type PyTypeBuilderCleanup = Box<dyn Fn(&PyTypeBuilder, *mut ffi::PyTypeObject)>;
117
118struct PyTypeBuilder {
119    slots: Vec<ffi::PyType_Slot>,
120    method_defs: Vec<ffi::PyMethodDef>,
121    member_defs: Vec<ffi::PyMemberDef>,
122    getset_builders: HashMap<&'static CStr, GetSetDefBuilder>,
123    /// Used to patch the type objects for the things there's no
124    /// PyType_FromSpec API for... there's no reason this should work,
125    /// except for that it does and we have tests.
126    #[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
127    cleanup: Vec<PyTypeBuilderCleanup>,
128    tp_base: *mut ffi::PyTypeObject,
129    tp_dealloc: ffi::destructor,
130    tp_dealloc_with_gc: ffi::destructor,
131    is_mapping: bool,
132    is_sequence: bool,
133    is_immutable_type: bool,
134    has_new: bool,
135    has_dealloc: bool,
136    has_getitem: bool,
137    has_setitem: bool,
138    has_traverse: bool,
139    has_clear: bool,
140    dict_offset: Option<PyObjectOffset>,
141    class_flags: c_ulong,
142    // Before Python 3.9, need to patch in buffer methods manually (they don't work in slots)
143    #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
144    buffer_procs: ffi::PyBufferProcs,
145}
146
147impl PyTypeBuilder {
148    /// # Safety
149    /// The given pointer must be of the correct type for the given slot
150    unsafe fn push_slot<T>(&mut self, slot: c_int, pfunc: *mut T) {
151        match slot {
152            ffi::Py_tp_new => self.has_new = true,
153            ffi::Py_tp_dealloc => self.has_dealloc = true,
154            ffi::Py_mp_subscript => self.has_getitem = true,
155            ffi::Py_mp_ass_subscript => self.has_setitem = true,
156            ffi::Py_tp_traverse => {
157                self.has_traverse = true;
158                self.class_flags |= ffi::Py_TPFLAGS_HAVE_GC;
159            }
160            ffi::Py_tp_clear => self.has_clear = true,
161            #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
162            ffi::Py_bf_getbuffer => {
163                // Safety: slot.pfunc is a valid function pointer
164                self.buffer_procs.bf_getbuffer =
165                    Some(unsafe { std::mem::transmute::<*mut T, ffi::getbufferproc>(pfunc) });
166            }
167            #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
168            ffi::Py_bf_releasebuffer => {
169                // Safety: slot.pfunc is a valid function pointer
170                self.buffer_procs.bf_releasebuffer =
171                    Some(unsafe { std::mem::transmute::<*mut T, ffi::releasebufferproc>(pfunc) });
172            }
173            _ => {}
174        }
175
176        self.slots.push(ffi::PyType_Slot {
177            slot,
178            pfunc: pfunc as _,
179        });
180    }
181
182    /// # Safety
183    /// It is the caller's responsibility that `data` is of the correct type for the given slot.
184    unsafe fn push_raw_vec_slot<T>(&mut self, slot: c_int, mut data: Vec<T>) {
185        if !data.is_empty() {
186            // Python expects a zeroed entry to mark the end of the defs
187            unsafe {
188                data.push(std::mem::zeroed());
189                self.push_slot(slot, Box::into_raw(data.into_boxed_slice()) as *mut c_void);
190            }
191        }
192    }
193
194    fn pymethod_def(&mut self, def: &PyMethodDefType) {
195        match def {
196            PyMethodDefType::Getter(getter) => self
197                .getset_builders
198                .entry(getter.name)
199                .or_default()
200                .add_getter(getter),
201            PyMethodDefType::Setter(setter) => self
202                .getset_builders
203                .entry(setter.name)
204                .or_default()
205                .add_setter(setter),
206            PyMethodDefType::Deleter(deleter) => self
207                .getset_builders
208                .entry(deleter.name)
209                .or_default()
210                .add_deleter(deleter),
211            PyMethodDefType::Method(def) => self.method_defs.push(def.into_raw()),
212            // These class attributes are added after the type gets created by LazyStaticType
213            PyMethodDefType::ClassAttribute(_) => {}
214            PyMethodDefType::StructMember(def) => self.member_defs.push(*def),
215        }
216    }
217
218    fn finalize_methods_and_properties(&mut self) -> Vec<GetSetDefType> {
219        let method_defs: Vec<pyo3_ffi::PyMethodDef> = std::mem::take(&mut self.method_defs);
220        // Safety: Py_tp_methods expects a raw vec of PyMethodDef
221        unsafe { self.push_raw_vec_slot(ffi::Py_tp_methods, method_defs) };
222
223        let member_defs = std::mem::take(&mut self.member_defs);
224        // Safety: Py_tp_members expects a raw vec of PyMemberDef
225        unsafe { self.push_raw_vec_slot(ffi::Py_tp_members, member_defs) };
226
227        let mut getset_destructors = Vec::with_capacity(self.getset_builders.len());
228
229        #[allow(unused_mut, reason = "not modified on PyPy")]
230        let mut property_defs: Vec<_> = self
231            .getset_builders
232            .iter()
233            .map(|(name, builder)| {
234                let (def, destructor) = builder.as_get_set_def(name);
235                getset_destructors.push(destructor);
236                def
237            })
238            .collect();
239
240        // PyPy automatically adds __dict__ getter / setter.
241        #[cfg(not(PyPy))]
242        // Supported on unlimited API for all versions, and on 3.9+ for limited API
243        #[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
244        if let Some(dict_offset) = self.dict_offset {
245            let get_dict;
246            let closure;
247            // PyObject_GenericGetDict not in the limited API until Python 3.10.
248            #[cfg(any(not(Py_LIMITED_API), Py_3_10))]
249            {
250                let _ = dict_offset;
251                get_dict = ffi::PyObject_GenericGetDict;
252                closure = ptr::null_mut();
253            }
254
255            // ... so we write a basic implementation ourselves
256            #[cfg(not(any(not(Py_LIMITED_API), Py_3_10)))]
257            {
258                extern "C" fn get_dict_impl(
259                    object: *mut ffi::PyObject,
260                    closure: *mut c_void,
261                ) -> *mut ffi::PyObject {
262                    unsafe {
263                        trampoline(|_| {
264                            let dict_offset = closure as ffi::Py_ssize_t;
265                            // we don't support negative dict_offset here; PyO3 doesn't set it negative
266                            assert!(dict_offset > 0);
267                            let dict_ptr =
268                                object.byte_offset(dict_offset).cast::<*mut ffi::PyObject>();
269                            if (*dict_ptr).is_null() {
270                                std::ptr::write(dict_ptr, ffi::PyDict_New());
271                            }
272                            Ok(ffi::compat::Py_XNewRef(*dict_ptr))
273                        })
274                    }
275                }
276
277                get_dict = get_dict_impl;
278                let PyObjectOffset::Absolute(offset) = dict_offset;
279                closure = offset as _;
280            }
281
282            property_defs.push(ffi::PyGetSetDef {
283                name: c"__dict__".as_ptr(),
284                get: Some(get_dict),
285                set: Some(ffi::PyObject_GenericSetDict),
286                doc: ptr::null(),
287                closure,
288            });
289        }
290
291        // Safety: Py_tp_getset expects a raw vec of PyGetSetDef
292        unsafe { self.push_raw_vec_slot(ffi::Py_tp_getset, property_defs) };
293
294        // If mapping methods implemented, define sequence methods get implemented too.
295        // CPython does the same for Python `class` statements.
296
297        // NB we don't implement sq_length to avoid annoying CPython behaviour of automatically adding
298        // the length to negative indices.
299
300        // Don't add these methods for "pure" mappings.
301
302        if !self.is_mapping && self.has_getitem {
303            // Safety: This is the correct slot type for Py_sq_item
304            unsafe {
305                self.push_slot(
306                    ffi::Py_sq_item,
307                    get_sequence_item_from_mapping as *mut c_void,
308                )
309            }
310        }
311
312        if !self.is_mapping && self.has_setitem {
313            // Safety: This is the correct slot type for Py_sq_ass_item
314            unsafe {
315                self.push_slot(
316                    ffi::Py_sq_ass_item,
317                    assign_sequence_item_from_mapping as *mut c_void,
318                )
319            }
320        }
321
322        getset_destructors
323    }
324
325    fn set_is_basetype(mut self, is_basetype: bool) -> Self {
326        if is_basetype {
327            self.class_flags |= ffi::Py_TPFLAGS_BASETYPE;
328        }
329        self
330    }
331
332    /// # Safety
333    /// All slots in the PyClassItemsIter should be correct
334    unsafe fn class_items(mut self, iter: PyClassItemsIter) -> Self {
335        for items in iter {
336            for slot in items.slots {
337                unsafe { self.push_slot(slot.slot, slot.pfunc) };
338            }
339            for method in items.methods {
340                self.pymethod_def(method);
341            }
342        }
343        self
344    }
345
346    fn type_doc(mut self, type_doc: &'static CStr) -> Self {
347        let slice = type_doc.to_bytes();
348        if !slice.is_empty() {
349            unsafe { self.push_slot(ffi::Py_tp_doc, type_doc.as_ptr() as *mut c_char) }
350
351            #[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
352            {
353                // Until CPython 3.10, tp_doc was treated specially for
354                // heap-types, and it removed the text_signature value from it.
355                // We go in after the fact and replace tp_doc with something
356                // that _does_ include the text_signature value!
357                self.cleanup
358                    .push(Box::new(move |_self, type_object| unsafe {
359                        ffi::PyObject_Free((*type_object).tp_doc as _);
360                        let data = ffi::PyMem_Malloc(slice.len());
361                        data.copy_from(slice.as_ptr() as _, slice.len());
362                        (*type_object).tp_doc = data as _;
363                    }))
364            }
365        }
366        self
367    }
368
369    fn offsets(
370        mut self,
371        dict_offset: Option<PyObjectOffset>,
372        #[allow(unused_variables)] weaklist_offset: Option<PyObjectOffset>,
373    ) -> Self {
374        self.dict_offset = dict_offset;
375
376        #[cfg(Py_3_9)]
377        {
378            #[inline(always)]
379            fn offset_def(name: &'static CStr, offset: PyObjectOffset) -> ffi::PyMemberDef {
380                let (offset, flags) = match offset {
381                    PyObjectOffset::Absolute(offset) => (offset, ffi::Py_READONLY),
382                    #[cfg(Py_3_12)]
383                    PyObjectOffset::Relative(offset) => {
384                        (offset, ffi::Py_READONLY | ffi::Py_RELATIVE_OFFSET)
385                    }
386                };
387                ffi::PyMemberDef {
388                    name: name.as_ptr().cast(),
389                    type_code: ffi::Py_T_PYSSIZET,
390                    offset,
391                    flags,
392                    doc: std::ptr::null_mut(),
393                }
394            }
395
396            // __dict__ support
397            if let Some(dict_offset) = dict_offset {
398                self.member_defs
399                    .push(offset_def(c"__dictoffset__", dict_offset));
400            }
401
402            // weakref support
403            if let Some(weaklist_offset) = weaklist_offset {
404                self.member_defs
405                    .push(offset_def(c"__weaklistoffset__", weaklist_offset));
406            }
407        }
408
409        // Setting buffer protocols, tp_dictoffset and tp_weaklistoffset via slots doesn't work until
410        // Python 3.9, so on older versions we must manually fixup the type object.
411        #[cfg(all(not(Py_LIMITED_API), not(Py_3_9)))]
412        {
413            self.cleanup
414                .push(Box::new(move |builder, type_object| unsafe {
415                    (*(*type_object).tp_as_buffer).bf_getbuffer = builder.buffer_procs.bf_getbuffer;
416                    (*(*type_object).tp_as_buffer).bf_releasebuffer =
417                        builder.buffer_procs.bf_releasebuffer;
418
419                    match dict_offset {
420                        Some(PyObjectOffset::Absolute(offset)) => {
421                            (*type_object).tp_dictoffset = offset;
422                        }
423                        None => {}
424                    }
425                    match weaklist_offset {
426                        Some(PyObjectOffset::Absolute(offset)) => {
427                            (*type_object).tp_weaklistoffset = offset;
428                        }
429                        None => {}
430                    }
431                }));
432        }
433        self
434    }
435
436    fn build(
437        mut self,
438        py: Python<'_>,
439        name: &'static str,
440        module_name: Option<&'static str>,
441        basicsize: ffi::Py_ssize_t,
442    ) -> PyResult<PyClassTypeObject> {
443        // `c_ulong` and `c_uint` have the same size
444        // on some platforms (like windows)
445        #![allow(clippy::useless_conversion)]
446
447        let getset_defs = self.finalize_methods_and_properties();
448
449        unsafe { self.push_slot(ffi::Py_tp_base, self.tp_base) }
450
451        if !self.has_new {
452            #[cfg(not(Py_3_10))]
453            {
454                // Safety: This is the correct slot type for Py_tp_new
455                unsafe { self.push_slot(ffi::Py_tp_new, no_constructor_defined as *mut c_void) }
456            }
457            #[cfg(Py_3_10)]
458            {
459                self.class_flags |= ffi::Py_TPFLAGS_DISALLOW_INSTANTIATION;
460            }
461        }
462
463        let base_is_gc = unsafe { ffi::PyType_IS_GC(self.tp_base) == 1 };
464        let tp_dealloc = if self.has_traverse || base_is_gc {
465            self.tp_dealloc_with_gc
466        } else {
467            self.tp_dealloc
468        };
469        unsafe { self.push_slot(ffi::Py_tp_dealloc, tp_dealloc as *mut c_void) }
470
471        if self.has_clear && !self.has_traverse {
472            return Err(PyTypeError::new_err(format!(
473                "`#[pyclass]` {name} implements __clear__ without __traverse__"
474            )));
475        }
476
477        // If this type is a GC type, and the base also is, we may need to add
478        // `tp_traverse` / `tp_clear` implementations to call the base, if this type didn't
479        // define `__traverse__` or `__clear__`.
480        //
481        // This is because when Py_TPFLAGS_HAVE_GC is set, then `tp_traverse` and
482        // `tp_clear` are not inherited.
483        if ((self.class_flags & ffi::Py_TPFLAGS_HAVE_GC) != 0) && base_is_gc {
484            // If this assertion breaks, need to consider doing the same for __traverse__.
485            assert!(self.has_traverse); // Py_TPFLAGS_HAVE_GC is set when a `__traverse__` method is found
486
487            if !self.has_clear {
488                // Safety: This is the correct slot type for Py_tp_clear
489                unsafe { self.push_slot(ffi::Py_tp_clear, call_super_clear as *mut c_void) }
490            }
491        }
492
493        // For sequences, implement sq_length instead of mp_length
494        if self.is_sequence {
495            for slot in &mut self.slots {
496                if slot.slot == ffi::Py_mp_length {
497                    slot.slot = ffi::Py_sq_length;
498                }
499            }
500        }
501
502        // Add empty sentinel at the end
503        // Safety: python expects this empty slot
504        unsafe { self.push_slot(0, ptr::null_mut::<c_void>()) }
505
506        let class_name = py_class_qualified_name(module_name, name)?;
507        let mut spec = ffi::PyType_Spec {
508            name: class_name.as_ptr() as _,
509            basicsize: basicsize as c_int,
510            itemsize: 0,
511
512            flags: (ffi::Py_TPFLAGS_DEFAULT | self.class_flags)
513                .try_into()
514                .unwrap(),
515            slots: self.slots.as_mut_ptr(),
516        };
517
518        // SAFETY: We've correctly setup the PyType_Spec at this point
519        // The FFI call is known to return a new type object or null on error
520        let type_object = unsafe {
521            ffi::PyType_FromSpec(&mut spec)
522                .assume_owned_or_err(py)?
523                .cast_into_unchecked::<PyType>()
524        };
525
526        #[cfg(not(Py_3_11))]
527        bpo_45315_workaround(py, class_name);
528
529        #[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
530        for cleanup in std::mem::take(&mut self.cleanup) {
531            cleanup(&self, type_object.as_type_ptr());
532        }
533
534        Ok(PyClassTypeObject {
535            type_object: type_object.unbind(),
536            is_immutable_type: self.is_immutable_type,
537            getset_defs,
538        })
539    }
540}
541
542fn py_class_qualified_name(module_name: Option<&str>, class_name: &str) -> PyResult<CString> {
543    Ok(CString::new(format!(
544        "{}.{}",
545        module_name.unwrap_or("builtins"),
546        class_name
547    ))?)
548}
549
550/// Workaround for Python issue 45315; no longer necessary in Python 3.11
551#[inline]
552#[cfg(not(Py_3_11))]
553fn bpo_45315_workaround(py: Python<'_>, class_name: CString) {
554    #[cfg(Py_LIMITED_API)]
555    {
556        // Must check version at runtime for abi3 wheels - they could run against a higher version
557        // than the build config suggests.
558        use crate::sync::PyOnceLock;
559        static IS_PYTHON_3_11: PyOnceLock<bool> = PyOnceLock::new();
560
561        if *IS_PYTHON_3_11.get_or_init(py, || py.version_info() >= (3, 11)) {
562            // No fix needed - the wheel is running on a sufficiently new interpreter.
563            return;
564        }
565    }
566    #[cfg(not(Py_LIMITED_API))]
567    {
568        // suppress unused variable warning
569        let _ = py;
570    }
571
572    std::mem::forget(class_name);
573}
574
575/// Default new implementation
576#[cfg(not(Py_3_10))]
577unsafe extern "C" fn no_constructor_defined(
578    subtype: *mut ffi::PyTypeObject,
579    _args: *mut ffi::PyObject,
580    _kwds: *mut ffi::PyObject,
581) -> *mut ffi::PyObject {
582    unsafe {
583        trampoline(|py| {
584            let tpobj = PyType::from_borrowed_type_ptr(py, subtype);
585            // unlike `fully_qualified_name`, this always include the module
586            let module = tpobj
587                .module()
588                .map_or_else(|_| "<unknown>".into(), |s| s.to_string());
589            let qualname = tpobj.qualname();
590            let qualname = qualname.map_or_else(|_| "<unknown>".into(), |s| s.to_string());
591            Err(crate::exceptions::PyTypeError::new_err(format!(
592                "cannot create '{module}.{qualname}' instances"
593            )))
594        })
595    }
596}
597
598unsafe extern "C" fn call_super_clear(slf: *mut ffi::PyObject) -> c_int {
599    unsafe { _call_clear(slf, |_, _| Ok(()), call_super_clear) }
600}
601
602#[derive(Default)]
603struct GetSetDefBuilder {
604    doc: Option<&'static CStr>,
605    getter: Option<Getter>,
606    setter: Option<Setter>,
607    deleter: Option<Deleter>,
608}
609
610impl GetSetDefBuilder {
611    fn add_getter(&mut self, getter: &PyGetterDef) {
612        // TODO: be smarter about merging getter and setter docs
613        if self.doc.is_none() {
614            self.doc = getter.doc;
615        }
616        // TODO: return an error if getter already defined?
617        self.getter = Some(getter.meth)
618    }
619
620    fn add_setter(&mut self, setter: &PySetterDef) {
621        // TODO: be smarter about merging getter and setter docs
622        if self.doc.is_none() {
623            self.doc = setter.doc;
624        }
625        // TODO: return an error if setter already defined?
626        self.setter = Some(setter.meth)
627    }
628
629    fn add_deleter(&mut self, deleter: &PyDeleterDef) {
630        // TODO: be smarter about merging getter, setter and deleter docs
631        if self.doc.is_none() {
632            self.doc = deleter.doc;
633        }
634        // TODO: return an error if deleter already defined?
635        self.deleter = Some(deleter.meth)
636    }
637
638    fn as_get_set_def(&self, name: &'static CStr) -> (ffi::PyGetSetDef, GetSetDefType) {
639        let getset_type = match (self.getter, self.setter, self.deleter) {
640            (None, None, None) => {
641                unreachable!("GetSetDefBuilder expected to always have either getter or setter")
642            }
643            (Some(getter), None, None) => GetSetDefType::Getter(getter),
644            (None, Some(setter), None) => GetSetDefType::Setter(setter),
645            (getter, setter, deleter) => {
646                GetSetDefType::Combination(Box::new(GetSetDeleteCombination {
647                    getter,
648                    setter,
649                    deleter,
650                }))
651            }
652        };
653
654        let getset_def = getset_type.create_py_get_set_def(name, self.doc);
655        (getset_def, getset_type)
656    }
657}
658
659/// Possible forms of property - either a getter, setter, or both
660enum GetSetDefType {
661    Getter(Getter),
662    Setter(Setter),
663    // The box is here so that the `GetSetDeleteCombination` has a stable
664    // memory address even if the `GetSetDeleteCombination` enum is moved
665    Combination(Box<GetSetDeleteCombination>),
666}
667
668pub(crate) struct GetSetDeleteCombination {
669    getter: Option<Getter>,
670    setter: Option<Setter>,
671    deleter: Option<Deleter>,
672}
673
674impl GetSetDefType {
675    /// Fills a PyGetSetDef structure
676    /// It is only valid for as long as this GetSetDefType remains alive,
677    /// as well as name and doc members
678    pub(crate) fn create_py_get_set_def(
679        &self,
680        name: &CStr,
681        doc: Option<&CStr>,
682    ) -> ffi::PyGetSetDef {
683        let (get, set, closure): (Option<ffi::getter>, Option<ffi::setter>, *mut c_void) =
684            match self {
685                &Self::Getter(closure) => {
686                    unsafe extern "C" fn getter(
687                        slf: *mut ffi::PyObject,
688                        closure: *mut c_void,
689                    ) -> *mut ffi::PyObject {
690                        let slf = unsafe { NonNull::new_unchecked(slf) };
691                        // Safety: PyO3 sets the closure when constructing the ffi getter so this cast should always be valid
692                        let getter: Getter = unsafe { std::mem::transmute(closure) };
693                        unsafe { trampoline(|py| getter(py, slf)) }
694                    }
695                    (Some(getter), None, closure as Getter as _)
696                }
697                &Self::Setter(closure) => {
698                    unsafe extern "C" fn setter(
699                        slf: *mut ffi::PyObject,
700                        value: *mut ffi::PyObject,
701                        closure: *mut c_void,
702                    ) -> c_int {
703                        let slf = unsafe { NonNull::new_unchecked(slf) };
704                        // Safety: PyO3 sets the closure when constructing the ffi setter so this cast should always be valid
705                        let setter: Setter = unsafe { std::mem::transmute(closure) };
706                        unsafe {
707                            trampoline(|py| {
708                                if let Some(value) = NonNull::new(value) {
709                                    setter(py, slf, value)
710                                } else {
711                                    Err(PyAttributeError::new_err("property has no deleter"))
712                                }
713                            })
714                        }
715                    }
716                    (None, Some(setter), closure as Setter as _)
717                }
718                Self::Combination(closure) => {
719                    unsafe extern "C" fn getset_getter(
720                        slf: *mut ffi::PyObject,
721                        closure: *mut c_void,
722                    ) -> *mut ffi::PyObject {
723                        let slf = unsafe { NonNull::new_unchecked(slf) };
724                        let getset: &GetSetDeleteCombination = unsafe { &*closure.cast() };
725                        // we only call this method if getter is set
726                        unsafe { trampoline(|py| getset.getter.unwrap_unchecked()(py, slf)) }
727                    }
728
729                    unsafe extern "C" fn getset_setter(
730                        slf: *mut ffi::PyObject,
731                        value: *mut ffi::PyObject,
732                        closure: *mut c_void,
733                    ) -> c_int {
734                        let slf = unsafe { NonNull::new_unchecked(slf) };
735                        let getset: &GetSetDeleteCombination = unsafe { &*closure.cast() };
736                        unsafe {
737                            trampoline(|py| {
738                                if let Some(value) = NonNull::new(value) {
739                                    getset.setter.ok_or_else(|| {
740                                        PyAttributeError::new_err("property has no setter")
741                                    })?(py, slf, value)
742                                } else {
743                                    getset.deleter.ok_or_else(|| {
744                                        PyAttributeError::new_err("property has no deleter")
745                                    })?(py, slf)
746                                }
747                            })
748                        }
749                    }
750                    (
751                        closure.getter.is_some().then_some(getset_getter),
752                        Some(getset_setter),
753                        NonNull::<GetSetDeleteCombination>::from(closure.as_ref())
754                            .cast()
755                            .as_ptr(),
756                    )
757                }
758            };
759        ffi::PyGetSetDef {
760            name: name.as_ptr(),
761            doc: doc.map_or(ptr::null(), CStr::as_ptr),
762            get,
763            set,
764            closure,
765        }
766    }
767}