Skip to main content

rustpython_vm/
class.rs

1//! Utilities to define a new Python class
2
3use crate::{
4    AsObject, PyPayload,
5    builtins::{PyBaseObject, PyType, PyTypeRef, descriptor::PyWrapper},
6    function::{ItemDoc, PyMethodDef, plain_doc},
7    object::Py,
8    types::{PyTypeFlags, PyTypeSlots, SLOT_DEFS, fn_addr, hash_not_implemented},
9    vm::{Context, VirtualMachine},
10};
11use rustpython_common::static_cell;
12
13/// Add slot wrapper descriptors to a type's dict
14///
15/// Iterates SLOT_DEFS and creates a PyWrapper for each slot that:
16/// 1. Has a function set in the type's slots
17/// 2. Doesn't already have an attribute in the type's dict
18#[cfg(feature = "doc")]
19pub fn add_operators(class: &'static Py<PyType>, ctx: &Context, attr_docs: &[(&str, u32, u32)]) {
20    add_operators_inner(class, ctx, |name| match attr_doc(attr_docs, name) {
21        Some((offset, len)) if len != 0 => (offset, len),
22        _ => (0, 0),
23    })
24}
25
26#[cfg(not(feature = "doc"))]
27pub fn add_operators(class: &'static Py<PyType>, ctx: &Context, attr_docs: &[&str]) {
28    add_operators_inner(class, ctx, |name| {
29        if attr_name_present(attr_docs, name) {
30            // Present in the database: do not fall back to the slot text.
31            (0, 1)
32        } else {
33            (0, 0)
34        }
35    })
36}
37
38fn add_operators_inner(
39    class: &'static Py<PyType>,
40    ctx: &Context,
41    mut plain_span: impl FnMut(&str) -> (u32, u32),
42) {
43    for def in SLOT_DEFS {
44        let (plain_off, plain_len) = plain_span(def.name);
45        // Skip __new__ - it has special handling
46        if def.name == "__new__" {
47            continue;
48        }
49
50        // Special handling for __hash__ = None
51        if def.name == "__hash__"
52            && class.slots().hash.load().is_some_and(|h| {
53                fn_addr(h) == fn_addr(hash_not_implemented as crate::types::HashFunc)
54            })
55        {
56            class.set_attr(ctx.names.__hash__, ctx.none.clone().into());
57            continue;
58        }
59
60        // __getattr__ should only have a wrapper if the type explicitly defines it.
61        // Unlike __getattribute__, __getattr__ is not present on object by default.
62        // Both map to TpGetattro, but only __getattribute__ gets a wrapper from the slot.
63        if def.name == "__getattr__" {
64            continue;
65        }
66
67        // Get the slot function wrapped in SlotFunc
68        let Some(slot_func) = def.accessor.get_slot_func_with_op(class.slots(), def.op) else {
69            continue;
70        };
71
72        // Check if attribute already exists in dict
73        let attr_name = ctx.intern_str(def.name);
74        if class.attributes().contains(attr_name) {
75            continue;
76        }
77
78        // Create and add the wrapper
79        let wrapper = PyWrapper {
80            typ: class,
81            name: attr_name,
82            wrapped: slot_func,
83            doc: Some(def.doc),
84            plain_off,
85            plain_len,
86        };
87        class.set_attr(attr_name, wrapper.into_ref(ctx).into());
88    }
89}
90
91pub trait StaticType {
92    // Ideally, saving PyType is better than PyTypeRef
93    fn static_cell() -> &'static static_cell::StaticCell<PyTypeRef>;
94
95    #[inline]
96    #[must_use]
97    fn static_metaclass() -> &'static Py<PyType> {
98        PyType::static_type()
99    }
100
101    #[inline]
102    #[must_use]
103    fn static_baseclass() -> &'static Py<PyType> {
104        PyBaseObject::static_type()
105    }
106
107    #[inline]
108    #[must_use]
109    fn static_type() -> &'static Py<PyType> {
110        #[cold]
111        fn fail() -> ! {
112            panic!(
113                "static type has not been initialized. e.g. the native types defined in different module may be used before importing library."
114            );
115        }
116        Self::static_cell().get().unwrap_or_else(|| fail())
117    }
118
119    #[must_use]
120    fn init_manually(typ: PyTypeRef) -> &'static Py<PyType> {
121        let cell = Self::static_cell();
122        cell.set(typ)
123            .unwrap_or_else(|_| panic!("double initialization from init_manually"));
124        let typ = cell.get().unwrap();
125        typ.as_object().make_immortal();
126        typ
127    }
128
129    #[must_use]
130    fn init_builtin_type() -> &'static Py<PyType>
131    where
132        Self: PyClassImpl,
133    {
134        let typ = Self::create_static_type();
135        let cell = Self::static_cell();
136        cell.set(typ)
137            .unwrap_or_else(|_| panic!("double initialization of {}", Self::NAME));
138        let typ = cell.get().unwrap();
139        typ.as_object().make_immortal();
140        typ
141    }
142
143    #[must_use]
144    fn create_static_type() -> PyTypeRef
145    where
146        Self: PyClassImpl,
147    {
148        // inherit_special COPYVAL(tp_itemsize): the direct base, and only when
149        // this type left the slot at 0. The base type object already exists.
150        let mut slots = Self::make_slots();
151        if slots.itemsize == 0 {
152            slots.itemsize = Self::static_baseclass().slots.itemsize;
153        }
154        PyType::new_static(
155            Self::static_baseclass().to_owned(),
156            Default::default(),
157            slots,
158            Self::static_metaclass().to_owned(),
159        )
160        .unwrap()
161    }
162}
163
164pub trait PyClassDef {
165    const NAME: &'static str;
166    const MODULE_NAME: Option<&'static str>;
167    const TP_NAME: &'static str;
168    const DOC: ItemDoc = ItemDoc::NONE;
169    /// Attribute name → database span, sorted by name.
170    /// `(u32::MAX, 0)` is an explicit empty doc.
171    #[cfg(feature = "doc")]
172    const ATTR_DOCS: &'static [(&'static str, u32, u32)] = &[];
173    /// Names that have a database doc, sorted. The text is not in this build.
174    #[cfg(not(feature = "doc"))]
175    const ATTR_DOCS: &'static [&'static str] = &[];
176    const BASICSIZE: usize;
177    const ITEMSIZE: usize = 0;
178    const UNHASHABLE: bool = false;
179    const MEMBERS: &'static [crate::builtins::descriptor::PyMemberSpec] = &[];
180
181    fn assert_member_layout() {}
182
183    // due to restriction of rust trait system, object.__base__ is None
184    // but PyBaseObject::Base will be PyBaseObject.
185    type Base: PyClassDef;
186}
187
188const fn cmp_str(left: &str, right: &str) -> i8 {
189    let left = left.as_bytes();
190    let right = right.as_bytes();
191    let n = if left.len() < right.len() {
192        left.len()
193    } else {
194        right.len()
195    };
196    let mut i = 0;
197    while i < n {
198        if left[i] != right[i] {
199            return if left[i] < right[i] { -1 } else { 1 };
200        }
201        i += 1;
202    }
203    if left.len() == right.len() {
204        0
205    } else if left.len() < right.len() {
206        -1
207    } else {
208        1
209    }
210}
211
212#[must_use]
213pub const fn attr_name_present(table: &[&str], name: &str) -> bool {
214    let mut lo = 0;
215    let mut hi = table.len();
216    while lo < hi {
217        let mid = (lo + hi) / 2;
218        let ord = cmp_str(table[mid], name);
219        if ord == 0 {
220            return true;
221        } else if ord < 0 {
222            lo = mid + 1;
223        } else {
224            hi = mid;
225        }
226    }
227    false
228}
229
230/// Database doc of attribute `name` of `T`.
231#[must_use]
232pub fn class_attr_item_doc<T: PyClassDef + ?Sized>(name: &str) -> ItemDoc {
233    #[cfg(feature = "doc")]
234    if let Some((offset, len)) = attr_doc(T::ATTR_DOCS, name) {
235        if len != 0 {
236            return ItemDoc {
237                text: None,
238                offset,
239                len,
240            };
241        }
242        if offset == u32::MAX {
243            return ItemDoc::EMPTY;
244        }
245    }
246    #[cfg(not(feature = "doc"))]
247    let _ = name;
248    ItemDoc::NONE
249}
250
251/// Set `doc` as `__doc__` of a native type that has none.
252pub fn assign_missing_doc(vm: &VirtualMachine, class: &Py<PyType>, doc: ItemDoc) {
253    let Some(text) = plain_doc(doc) else {
254        return;
255    };
256    let doc_name = identifier!(vm, __doc__);
257    let missing = class
258        .attributes()
259        .get(doc_name)
260        .is_none_or(|value| value.is(&vm.ctx.none));
261    if missing {
262        class.set_attr(doc_name, vm.ctx.new_str(text).into());
263    }
264}
265
266/// Doc for `name` in a sorted attribute-doc table.
267#[must_use]
268#[inline(never)]
269pub const fn attr_doc(table: &[(&str, u32, u32)], name: &str) -> Option<(u32, u32)> {
270    let mut lo = 0;
271    let mut hi = table.len();
272    while lo < hi {
273        let mid = (lo + hi) / 2;
274        let ord = cmp_str(table[mid].0, name);
275        if ord == 0 {
276            return Some((table[mid].1, table[mid].2));
277        } else if ord < 0 {
278            lo = mid + 1;
279        } else {
280            hi = mid;
281        }
282    }
283    None
284}
285
286pub trait PyClassImpl: PyClassDef {
287    const TP_FLAGS: PyTypeFlags = PyTypeFlags::empty();
288
289    /// Signature-bearing class doc. [`ItemDoc::NONE`] when the constructor has no signature.
290    const INTERNAL_DOC: ItemDoc = ItemDoc::NONE;
291
292    const METHOD_DEFS: &'static [PyMethodDef];
293
294    fn impl_extend_class(ctx: &'static Context, class: &'static Py<PyType>);
295
296    fn extend_slots(slots: &mut PyTypeSlots);
297
298    fn extend_class(ctx: &'static Context, class: &'static Py<PyType>)
299    where
300        Self: Sized,
301    {
302        #[cfg(debug_assertions)]
303        debug_assert!(
304            class
305                .slots()
306                .flags
307                .contains(&PyTypeFlags::_CREATED_WITH_FLAGS)
308        );
309
310        let _ = ctx.intern_str(Self::NAME); // intern type name
311
312        if Self::TP_FLAGS.contains(&PyTypeFlags::HAS_DICT)
313            && !Self::MEMBERS.iter().any(|member| member.name == "__dict__")
314        {
315            let __dict__ = identifier!(ctx, __dict__);
316            class.set_attr(
317                __dict__,
318                ctx.new_static_getset(
319                    "__dict__",
320                    class,
321                    crate::builtins::object::object_get_dict,
322                    crate::builtins::object::object_set_dict,
323                )
324                .into(),
325            );
326        }
327
328        Self::assert_member_layout();
329        for member in Self::MEMBERS {
330            class.set_str_attr(
331                member.name,
332                ctx.new_member(
333                    member.name,
334                    member.kind,
335                    member.offset,
336                    member.flags,
337                    class,
338                    member.doc,
339                ),
340                ctx,
341            );
342        }
343
344        Self::impl_extend_class(ctx, class);
345
346        // Only set __doc__ if it doesn't already exist (e.g., as a member descriptor)
347        // This matches CPython's behavior in type_dict_set_doc
348        let doc_attr_name = identifier!(ctx, __doc__);
349        if class.attributes().get(doc_attr_name).is_none() {
350            let doc =
351                plain_doc(Self::DOC).map_or_else(|| ctx.none(), |doc| ctx.new_str(doc).into());
352            class.set_attr(doc_attr_name, doc);
353        }
354
355        if let Some(module_name) = Self::MODULE_NAME {
356            let module_key = identifier!(ctx, __module__);
357            // Don't overwrite a getset descriptor for __module__ (e.g. TypeAliasType
358            // has an instance-level __module__ getset that should not be replaced)
359            let has_getset = class
360                .attributes()
361                .get(module_key)
362                .is_some_and(|v| v.downcastable::<crate::builtins::PyGetSet>());
363            if !has_getset {
364                class.set_attr(module_key, ctx.new_str(module_name).into());
365            }
366        }
367
368        // Don't add __new__ attribute if slot_new is inherited from object
369        // (Python doesn't add __new__ to __dict__ for inherited slots)
370        // Exception: object itself should have __new__ in its dict
371        if let Some(slot_new) = class.slots().new.load() {
372            let object_new = ctx.types.object_type.slots().new.load();
373            let is_object_itself = core::ptr::eq(class, ctx.types.object_type);
374            let is_inherited_from_object = !is_object_itself
375                && object_new.is_some_and(|obj_new| fn_addr(slot_new) == fn_addr(obj_new));
376
377            if !is_inherited_from_object {
378                let bound_new =
379                    ctx.slot_new_wrapper
380                        .build_bound_method(ctx, class.to_owned().into(), class);
381                class.set_attr(identifier!(ctx, __new__), bound_new.into());
382            }
383        }
384
385        // Add slot wrappers using SLOT_DEFS array
386        add_operators(class, ctx, Self::ATTR_DOCS);
387
388        // Same walk as init_slots: a slot such as tp_init is copied only from
389        // a base that defines it, so a static grandchild must see that base
390        // in the MRO, not only its direct bases.
391        let mro = {
392            let guard = class.mro.read();
393            guard[1..].to_vec()
394        };
395        for base in &mro {
396            class.inherit_slots(base);
397        }
398
399        class.extend_methods(class.slots().methods, ctx);
400    }
401
402    #[must_use]
403    fn make_static_type() -> PyTypeRef
404    where
405        Self: StaticType + Sized,
406    {
407        let typ = Self::static_cell().get_or_init(|| {
408            let typ = Self::create_static_type();
409            Self::extend_class(Context::genesis(), unsafe {
410                // typ will be saved in static_cell
411                let r: &Py<PyType> = &typ;
412                let r: &'static Py<PyType> = core::mem::transmute(r);
413                r
414            });
415            typ
416        });
417        // A static type is held by its `static_cell` for the life of the
418        // process, so nothing is kept alive that would have died: all this
419        // buys is that every reference to a builtin type from here on is a
420        // branch rather than an atomic read-modify-write.
421        typ.as_object().make_immortal();
422        (*typ).to_owned()
423    }
424
425    fn make_slots() -> PyTypeSlots {
426        let mut slots = PyTypeSlots {
427            flags: crate::types::AtomicPyTypeFlags::from_plain(Self::TP_FLAGS),
428            name: Self::TP_NAME,
429            basicsize: Self::BASICSIZE,
430            itemsize: Self::ITEMSIZE,
431            doc: {
432                let internal = Self::INTERNAL_DOC;
433                if internal.text.is_some() || internal.len != 0 {
434                    internal
435                } else {
436                    Self::DOC
437                }
438            },
439            methods: Self::METHOD_DEFS,
440            ..Default::default()
441        };
442
443        if Self::UNHASHABLE {
444            slots.hash.store(Some(hash_not_implemented));
445        }
446
447        Self::extend_slots(&mut slots);
448        slots
449    }
450}
451
452/// Trait for Python subclasses that can provide a reference to their base type.
453///
454/// This trait is automatically implemented by the `#[pyclass]` macro when
455/// `base = SomeType` is specified. It provides safe reference access to the
456/// base type's payload.
457///
458/// For subclasses with `#[repr(transparent)]`
459/// which enables ownership transfer via `into_base()`.
460pub trait PySubclass: crate::PyPayload {
461    type Base: crate::PyPayload;
462
463    /// Returns a reference to the base type's payload.
464    fn as_base(&self) -> &Self::Base;
465}