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#[deprecated = "Renamed to `ClassId`"]
39pub type ClassName = ClassId;
40
41/// Globally unique ID of a class registered with Godot.
42///
43/// This struct implements `Copy` and is very cheap to copy and compare with other `ClassId`s.
44///
45/// `ClassId` can also be used to obtain the class name, which is cached globally, not per-instance. Note that it holds the Godot name,
46/// not the Rust name -- they sometimes differ, e.g. Godot `CSGMesh3D` vs Rust `CsgMesh3D`.
47///
48/// You can access existing classes' ID using [`GodotClass::class_id()`][crate::obj::GodotClass::class_id].
49/// If you need to create your own class ID, use [`new_cached()`][Self::new_cached].
50///
51/// # Ordering
52///
53/// `ClassId`s are **not** ordered lexicographically, and the ordering relation is **not** stable across multiple runs of your
54/// application. When lexicographical order is needed, it's possible to convert this type to [`GString`] or [`String`]. Note that
55/// [`StringName`] does not implement `Ord`, and its Godot comparison operators are not lexicographical either.
56#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
57pub struct ClassId {
58    global_index: u16,
59}
60
61impl ClassId {
62    /// Construct a new class name.
63    ///
64    /// You should typically only need this when implementing `GodotClass` manually, without `#[derive(GodotClass)]`, and overriding
65    /// `class_id()`. To access an existing type's class name, use [`<T as GodotClass>::class_id()`][crate::obj::GodotClass::class_name].
66    ///
67    /// 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
68    /// store the result in a `static`, to further reduce lookup times, but it's not required.
69    ///
70    /// We discourage calling this function from different places for the same `T`. But if you do so, `init_fn` must return the same string.
71    ///
72    /// # Panics
73    /// If the string is not ASCII and the Godot version is older than 4.4. From Godot 4.4 onwards, class names can be Unicode.
74    pub fn new_cached<T: GodotClass>(init_fn: impl FnOnce() -> String) -> Self {
75        Self::new_cached_inner::<T>(init_fn)
76    }
77
78    // Without bounds.
79    fn new_cached_inner<T: 'static>(init_fn: impl FnOnce() -> String) -> ClassId {
80        let type_id = TypeId::of::<T>();
81        let mut cache = CLASS_ID_CACHE.lock();
82
83        // Check if already cached by type
84        if let Some(global_index) = cache.get_by_type_id(type_id) {
85            return ClassId { global_index };
86        }
87
88        // Not cached, need to get or create entry
89        let name = init_fn();
90
91        #[cfg(before_api = "4.4")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.4")))]
92        assert!(
93            name.is_ascii(),
94            "In Godot < 4.4, class name must be ASCII: '{name}'"
95        );
96
97        cache.insert_class_id(Cow::Owned(name), Some(type_id), false)
98    }
99
100    /// Create a `ClassId` from a runtime string (for dynamic class names).
101    ///
102    /// Will reuse existing `ClassId` entries if the string is recognized.
103    // Deliberately not public.
104    pub(crate) fn new_dynamic(class_name: String) -> Self {
105        let mut cache = CLASS_ID_CACHE.lock();
106
107        cache.insert_class_id(Cow::Owned(class_name), None, false)
108    }
109
110    // Test-only APIs.
111    #[cfg(feature = "trace")] // itest only.
112    #[doc(hidden)]
113    pub fn __cached<T: 'static>(init_fn: impl FnOnce() -> String) -> Self {
114        Self::new_cached_inner::<T>(init_fn)
115    }
116
117    #[cfg(feature = "trace")] // itest only.
118    #[doc(hidden)]
119    pub fn __dynamic(class_name: &str) -> Self {
120        Self::new_dynamic(class_name.to_string())
121    }
122
123    #[doc(hidden)]
124    pub fn none() -> Self {
125        // First element is always the empty string name.
126        Self { global_index: 0 }
127    }
128
129    /// Create a new Unicode entry; expect to be unique. Internal, reserved for macros.
130    #[doc(hidden)]
131    pub fn __alloc_next_unicode(class_name_str: &'static str) -> Self {
132        #[cfg(before_api = "4.4")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.4")))]
133        assert!(
134            class_name_str.is_ascii(),
135            "Before Godot 4.4, class names must be ASCII, but '{class_name_str}' is not.\nSee https://github.com/godotengine/godot/pull/96501."
136        );
137
138        let source = Cow::Borrowed(class_name_str);
139        let mut cache = CLASS_ID_CACHE.lock();
140        cache.insert_class_id(source, None, true)
141    }
142
143    #[doc(hidden)]
144    pub fn is_none(&self) -> bool {
145        self.global_index == 0
146    }
147
148    /// Returns the class name as a `GString`.
149    pub fn to_gstring(&self) -> GString {
150        self.with_string_name(|s| s.into())
151    }
152
153    /// Returns the class name as a `StringName`.
154    pub fn to_string_name(&self) -> StringName {
155        self.with_string_name(|s| s.clone())
156    }
157
158    /// Returns an owned or borrowed `str` representing the class name.
159    pub fn to_cow_str(&self) -> Cow<'static, str> {
160        let cache = CLASS_ID_CACHE.lock();
161        let entry = cache.get_entry(self.global_index as usize);
162        entry.rust_str.clone()
163    }
164
165    /// The returned pointer is valid indefinitely, as entries are never deleted from the cache.
166    /// Since we use `Box<StringName>`, `HashMap` reallocations don't affect the validity of the StringName.
167    #[doc(hidden)]
168    pub fn string_sys(&self) -> sys::GDExtensionConstStringNamePtr {
169        self.with_string_name(|s| s.string_sys())
170    }
171
172    // Takes a closure because the mutex guard protects the reference; so the &StringName cannot leave the scope.
173    fn with_string_name<R>(&self, func: impl FnOnce(&StringName) -> R) -> R {
174        let cache = CLASS_ID_CACHE.lock();
175        let entry = cache.get_entry(self.global_index as usize);
176
177        let string_name = entry
178            .godot_str
179            .get_or_init(|| StringName::from(entry.rust_str.as_ref()));
180
181        func(string_name)
182    }
183}
184
185impl fmt::Display for ClassId {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        self.to_cow_str().fmt(f)
188    }
189}
190
191impl fmt::Debug for ClassId {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        let name = self.to_cow_str();
194
195        if name.is_empty() {
196            write!(f, "ClassId(none)")
197        } else {
198            write!(f, "ClassId({:?})", name)
199        }
200    }
201}
202
203// ----------------------------------------------------------------------------------------------------------------------------------------------
204
205/// Entry in the class name cache.
206///
207/// `StringName` needs to be lazy-initialized because the Godot binding may not be initialized yet.
208struct ClassIdEntry {
209    rust_str: Cow<'static, str>,
210    godot_str: OnceCell<StringName>,
211}
212
213impl ClassIdEntry {
214    fn new(rust_str: Cow<'static, str>) -> Self {
215        Self {
216            rust_str,
217            godot_str: OnceCell::new(),
218        }
219    }
220
221    fn none() -> Self {
222        Self::new(Cow::Borrowed(""))
223    }
224}
225
226// ----------------------------------------------------------------------------------------------------------------------------------------------
227
228/// Unified cache for all class name data.
229struct ClassIdCache {
230    /// All class name entries, with index representing [`ClassId::global_index`].
231    /// First element (index 0) is always the empty string name, which is used for "no class".
232    entries: Vec<ClassIdEntry>,
233    /// Cache for type-based lookups.
234    type_to_index: HashMap<TypeId, u16>,
235    /// Cache for runtime string-based lookups.
236    string_to_index: HashMap<String, u16>,
237}
238
239impl ClassIdCache {
240    fn new() -> Self {
241        let mut string_to_index = HashMap::new();
242        // Pre-populate string cache with the empty string at index 0.
243        string_to_index.insert(String::new(), 0);
244
245        Self {
246            entries: vec![ClassIdEntry::none()],
247            type_to_index: HashMap::new(),
248            string_to_index,
249        }
250    }
251
252    /// Looks up entries and if not present, inserts them.
253    ///
254    /// Returns the `ClassId` for the given name.
255    ///
256    /// # Panics (Debug)
257    /// If `expect_first` is true and the string is already present in the cache.
258    fn insert_class_id(
259        &mut self,
260        source: Cow<'static, str>,
261        type_id: Option<TypeId>,
262        expect_first: bool,
263    ) -> ClassId {
264        if expect_first {
265            // Debug verification that we're indeed the first to register this string.
266            #[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
267            assert!(
268                !self.string_to_index.contains_key(source.as_ref()),
269                "insert_class_name() called for already-existing string: {}",
270                source
271            );
272        } else {
273            // Check string cache first (dynamic path may reuse existing entries).
274            if let Some(&existing_index) = self.string_to_index.get(source.as_ref()) {
275                // Update type cache if we have a TypeId and it's not already cached (dynamic-then-static case).
276                // Note: if type_id is Some, we know it came from new_cached_inner after a failed TypeId lookup.
277                if let Some(type_id) = type_id {
278                    self.type_to_index.entry(type_id).or_insert(existing_index);
279                }
280                return ClassId {
281                    global_index: existing_index,
282                };
283            }
284        }
285
286        // Not found or static path - create new entry.
287        let global_index =
288            self.entries.len().try_into().unwrap_or_else(|_| {
289                panic!("ClassId cache exceeded maximum capacity of 65536 entries")
290            });
291
292        self.entries.push(ClassIdEntry::new(source.clone()));
293        self.string_to_index
294            .insert(source.into_owned(), global_index);
295
296        if let Some(type_id) = type_id {
297            self.type_to_index.insert(type_id, global_index);
298        }
299
300        ClassId { global_index }
301    }
302
303    fn get_by_type_id(&self, type_id: TypeId) -> Option<u16> {
304        self.type_to_index.get(&type_id).copied()
305    }
306
307    fn get_entry(&self, index: usize) -> &ClassIdEntry {
308        &self.entries[index]
309    }
310
311    fn clear(&mut self) {
312        // MACOS-PARTIAL-RELOAD: Previous implementation for when upstream fixes `.gdextension` reload.
313        // self.entries.clear();
314        // self.type_to_index.clear();
315        // self.string_to_index.clear();
316
317        // MACOS-PARTIAL-RELOAD: Preserve existing `ClassId` entries when only the `.gdextension` reloads so indices stay valid.
318        // There are two types of hot reload: `dylib` reload (`dylib` `mtime` newer) unloads and reloads the library, whereas
319        // `.gdextension` reload (`.gdextension` `mtime` newer) re-initializes the existing `dylib` without unloading it. To handle
320        // `.gdextension` reload, keep the backing entries (and thus the `string_to_index` map) but drop cached Godot `StringNames`
321        // and the `TypeId` lookup so they can be rebuilt.
322        for entry in &mut self.entries {
323            entry.godot_str = OnceCell::new();
324        }
325
326        self.type_to_index.clear();
327    }
328}