Skip to main content

godot_core/meta/
class_id.rs

1/*
2 * Copyright (c) godot-rust; Bromeon and contributors.
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6 */
7
8use std::any::TypeId;
9use std::borrow::Cow;
10use std::cell::OnceCell;
11use std::collections::HashMap;
12use std::fmt;
13use std::hash::Hash;
14
15use godot_ffi as sys;
16use sys::Global;
17
18use crate::builtin::*;
19use crate::obj::GodotClass;
20
21// Alternative optimizations:
22// - Small-array optimization for common string lengths.
23// - Use HashMap and store pre-computed hash. Would need a custom S parameter for HashMap<K, V, S>, see
24//   https://doc.rust-lang.org/std/hash/trait.BuildHasher.html (the default hasher recomputes the hash repeatedly).
25//
26
27/// Global cache of class names.
28static CLASS_ID_CACHE: Global<ClassIdCache> = Global::new(ClassIdCache::new);
29
30/// # Safety
31/// Must not use any `ClassId` APIs after this call.
32pub unsafe fn cleanup() {
33    CLASS_ID_CACHE.lock().clear();
34}
35
36// ----------------------------------------------------------------------------------------------------------------------------------------------
37
38/// Globally unique ID of a class registered with Godot.
39///
40/// This struct implements `Copy` and is very cheap to copy and compare with other `ClassId`s.
41///
42/// `ClassId` can also be used to obtain the class name, which is cached globally, not per-instance. Note that it holds the Godot name,
43/// not the Rust name -- they sometimes differ, e.g. Godot `CSGMesh3D` vs Rust `CsgMesh3D`.
44///
45/// You can access existing classes' ID using [`GodotClass::class_id()`][crate::obj::GodotClass::class_id].
46/// If you need to create your own class ID, use [`new_cached()`][Self::new_cached].
47///
48/// # Ordering
49///
50/// `ClassId`s are **not** ordered lexicographically, and the ordering relation is **not** stable across multiple runs of your
51/// application. When lexicographical order is needed, it's possible to convert this type to [`GString`] or [`String`]. Note that
52/// [`StringName`] does not implement `Ord`, and its Godot comparison operators are not lexicographical either.
53#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
54pub struct ClassId {
55    global_index: u16,
56}
57
58impl ClassId {
59    /// Construct a new class name.
60    ///
61    /// You should typically only need this when implementing `GodotClass` manually, without `#[derive(GodotClass)]`, and overriding
62    /// `class_id()`. To access an existing type's class name, use [`<T as GodotClass>::class_id()`][crate::obj::GodotClass::class_id].
63    ///
64    /// This function is expensive the first time it called for a given `T`, but will be cached for subsequent calls. It can make sense to
65    /// store the result in a `static`, to further reduce lookup times, but it's not required.
66    ///
67    /// We discourage calling this function from different places for the same `T`. But if you do so, `init_fn` must return the same string.
68    ///
69    /// # Panics
70    /// If the string contains non-ASCII characters and the Godot version is older than 4.4. From Godot 4.4 onwards, class names can be Unicode;
71    /// See <https://github.com/godotengine/godot/pull/96501>.
72    pub fn new_cached<T: GodotClass>(init_fn: impl FnOnce() -> String) -> Self {
73        Self::new_cached_inner::<T>(init_fn)
74    }
75
76    // Without bounds.
77    fn new_cached_inner<T: 'static>(init_fn: impl FnOnce() -> String) -> ClassId {
78        let type_id = TypeId::of::<T>();
79        let mut cache = CLASS_ID_CACHE.lock();
80
81        // Check if already cached by type
82        if let Some(global_index) = cache.get_by_type_id(type_id) {
83            return ClassId { global_index };
84        }
85
86        // Not cached, need to get or create entry
87        let name = init_fn();
88
89        #[cfg(before_api = "4.4")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.4")))]
90        assert!(
91            name.is_ascii(),
92            "In Godot < 4.4, class name must be ASCII: '{name}'"
93        );
94
95        cache.insert_class_id(Cow::Owned(name), Some(type_id), false)
96    }
97
98    /// Create a `ClassId` from a class name only known at runtime.
99    ///
100    /// Unlike [`ClassId::new_cached()`], this doesn't require a static type parameter. Useful for classes defined outside Rust code, e.g. in
101    /// scripts.
102    ///
103    /// Multiple calls with the same name return equal `ClassId` instances (but may need a lookup).
104    ///
105    /// # Example
106    /// ```no_run
107    /// use godot::meta::ClassId;
108    ///
109    /// let a = ClassId::new_dynamic("MyGDScriptClass");
110    /// let b = ClassId::new_dynamic("MyGDScriptClass");
111    /// assert_eq!(a, b);
112    /// ```
113    ///
114    /// # Panics
115    /// If the string contains non-ASCII characters and the Godot version is older than 4.4. From Godot 4.4 onwards, class names can be Unicode;
116    /// See <https://github.com/godotengine/godot/pull/96501>.
117    pub fn new_dynamic(class_name: impl Into<CowStr>) -> Self {
118        let mut cache = CLASS_ID_CACHE.lock();
119
120        cache.insert_class_id(class_name.into(), None, false)
121    }
122
123    // Test-only APIs.
124    #[cfg(feature = "trace")] // itest only.
125    #[doc(hidden)]
126    pub fn __cached<T: 'static>(init_fn: impl FnOnce() -> String) -> Self {
127        Self::new_cached_inner::<T>(init_fn)
128    }
129
130    #[cfg(feature = "trace")] // itest only.
131    #[doc(hidden)]
132    pub fn __dynamic(class_name: &str) -> Self {
133        Self::new_dynamic(class_name.to_string())
134    }
135
136    /// Returns a `ClassId` representing "no class" (empty class name) for non-object property types.
137    ///
138    /// This is used for properties that don't have an associated class, e.g. built-in types like `i32`, `GString`, `Vector3` etc.
139    /// When constructing a [`PropertyInfo`][crate::registry::info::PropertyInfo] for non-class types, you can use `StringName::default()`
140    /// for the `class_name` field.
141    pub fn none() -> Self {
142        // First element is always the empty string name.
143        Self { global_index: 0 }
144    }
145
146    /// Create a new Unicode entry; expect to be unique. Internal, reserved for macros.
147    #[doc(hidden)]
148    pub fn __alloc_next_unicode(class_name_str: &'static str) -> Self {
149        #[cfg(before_api = "4.4")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.4")))]
150        assert!(
151            class_name_str.is_ascii(),
152            "Before Godot 4.4, class names must be ASCII, but '{class_name_str}' is not.\nSee https://github.com/godotengine/godot/pull/96501."
153        );
154
155        let source = Cow::Borrowed(class_name_str);
156        let mut cache = CLASS_ID_CACHE.lock();
157        cache.insert_class_id(source, None, true)
158    }
159
160    #[doc(hidden)]
161    pub fn is_none(&self) -> bool {
162        self.global_index == 0
163    }
164
165    /// Returns the class name as a `GString`.
166    pub fn to_gstring(&self) -> GString {
167        self.with_string_name(|s| s.into())
168    }
169
170    /// Returns the class name as a `StringName`.
171    pub fn to_string_name(&self) -> StringName {
172        self.with_string_name(|s| s.clone())
173    }
174
175    /// Returns an owned or borrowed `str` representing the class name.
176    pub fn to_cow_str(&self) -> CowStr {
177        let cache = CLASS_ID_CACHE.lock();
178        let entry = cache.get_entry(self.global_index as usize);
179        entry.rust_str.clone()
180    }
181
182    /// The returned pointer is valid indefinitely, as entries are never deleted from the cache.
183    /// Since we use `Box<StringName>`, `HashMap` reallocations don't affect the validity of the StringName.
184    #[doc(hidden)]
185    pub fn string_sys(&self) -> sys::GDExtensionConstStringNamePtr {
186        self.with_string_name(|s| s.string_sys())
187    }
188
189    // Takes a closure because the mutex guard protects the reference; so the &StringName cannot leave the scope.
190    fn with_string_name<R>(&self, func: impl FnOnce(&StringName) -> R) -> R {
191        let cache = CLASS_ID_CACHE.lock();
192        let entry = cache.get_entry(self.global_index as usize);
193
194        let string_name = entry
195            .godot_str
196            .get_or_init(|| StringName::from(entry.rust_str.as_ref()));
197
198        func(string_name)
199    }
200}
201
202impl fmt::Display for ClassId {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        self.to_cow_str().fmt(f)
205    }
206}
207
208impl fmt::Debug for ClassId {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        let name = self.to_cow_str();
211
212        if name.is_empty() {
213            write!(f, "ClassId(none)")
214        } else {
215            write!(f, "ClassId({:?})", name)
216        }
217    }
218}
219
220// ----------------------------------------------------------------------------------------------------------------------------------------------
221
222/// Entry in the class name cache.
223///
224/// `StringName` needs to be lazy-initialized because the Godot binding may not be initialized yet.
225struct ClassIdEntry {
226    rust_str: CowStr,
227    godot_str: OnceCell<StringName>,
228}
229
230impl ClassIdEntry {
231    const fn new(rust_str: CowStr) -> Self {
232        Self {
233            rust_str,
234            godot_str: OnceCell::new(),
235        }
236    }
237
238    fn none() -> Self {
239        Self::new(Cow::Borrowed(""))
240    }
241}
242
243// ----------------------------------------------------------------------------------------------------------------------------------------------
244
245/// Unified cache for all class name data.
246struct ClassIdCache {
247    /// All class name entries, with index representing [`ClassId::global_index`].
248    /// First element (index 0) is always the empty string name, which is used for "no class".
249    entries: Vec<ClassIdEntry>,
250    /// Cache for type-based lookups.
251    type_to_index: HashMap<TypeId, u16>,
252    /// Cache for runtime string-based lookups.
253    string_to_index: HashMap<String, u16>,
254}
255
256impl ClassIdCache {
257    fn new() -> Self {
258        let mut string_to_index = HashMap::new();
259        // Pre-populate string cache with the empty string at index 0.
260        string_to_index.insert(String::new(), 0);
261
262        Self {
263            entries: vec![ClassIdEntry::none()],
264            type_to_index: HashMap::new(),
265            string_to_index,
266        }
267    }
268
269    /// Looks up entries and if not present, inserts them.
270    ///
271    /// Returns the `ClassId` for the given name.
272    ///
273    /// # Panics (safeguards-balanced)
274    /// If `expect_first` is true and the string is already present in the cache.
275    fn insert_class_id(
276        &mut self,
277        source: CowStr,
278        type_id: Option<TypeId>,
279        expect_first: bool,
280    ) -> ClassId {
281        if expect_first {
282            // From Rust edition 2024+, doctests are merged. Even with ```no_run, the link step (registration of class ID)
283            // still happens on test startup, so classes in other codeblocks with the same name cause panics in tests. To address this, we
284            // rely on the fact that Godot is not run during unit-tests, and thus is_initialized() is false. We then return the existing ID,
285            // which is wrong but doesn't matter as there's no runtime logic being performed. We do not support unit-tests without ```no_run.
286            if !sys::is_initialized()
287                && let Some(&existing_index) = self.string_to_index.get(source.as_ref())
288            {
289                return ClassId {
290                    global_index: existing_index,
291                };
292            }
293
294            // Debug verification that we're indeed the first to register this string.
295            sys::balanced_assert!(
296                !self.string_to_index.contains_key(source.as_ref()),
297                "insert_class_name() called for already-existing string: {}",
298                source
299            );
300        } else {
301            // Check string cache first (dynamic path may reuse existing entries).
302            if let Some(&existing_index) = self.string_to_index.get(source.as_ref()) {
303                // Update type cache if we have a TypeId and it's not already cached (dynamic-then-static case).
304                // Note: if type_id is Some, we know it came from new_cached_inner after a failed TypeId lookup.
305                if let Some(type_id) = type_id {
306                    self.type_to_index.entry(type_id).or_insert(existing_index);
307                }
308                return ClassId {
309                    global_index: existing_index,
310                };
311            }
312        }
313
314        // Not found or static path - create new entry.
315        let global_index =
316            self.entries.len().try_into().unwrap_or_else(|_| {
317                panic!("ClassId cache exceeded maximum capacity of 65536 entries")
318            });
319
320        self.entries.push(ClassIdEntry::new(source.clone()));
321        self.string_to_index
322            .insert(source.into_owned(), global_index);
323
324        if let Some(type_id) = type_id {
325            self.type_to_index.insert(type_id, global_index);
326        }
327
328        ClassId { global_index }
329    }
330
331    fn get_by_type_id(&self, type_id: TypeId) -> Option<u16> {
332        self.type_to_index.get(&type_id).copied()
333    }
334
335    fn get_entry(&self, index: usize) -> &ClassIdEntry {
336        &self.entries[index]
337    }
338
339    fn clear(&mut self) {
340        // MACOS-PARTIAL-RELOAD: Previous implementation for when upstream fixes `.gdextension` reload.
341        // self.entries.clear();
342        // self.type_to_index.clear();
343        // self.string_to_index.clear();
344
345        // MACOS-PARTIAL-RELOAD: Preserve existing `ClassId` entries when only the `.gdextension` reloads so indices stay valid.
346        // There are two types of hot reload: `dylib` reload (`dylib` `mtime` newer) unloads and reloads the library, whereas
347        // `.gdextension` reload (`.gdextension` `mtime` newer) re-initializes the existing `dylib` without unloading it. To handle
348        // `.gdextension` reload, keep the backing entries (and thus the `string_to_index` map) but drop cached Godot `StringNames`
349        // and the `TypeId` lookup so they can be rebuilt.
350        for entry in &mut self.entries {
351            entry.godot_str = OnceCell::new();
352        }
353
354        self.type_to_index.clear();
355    }
356}