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}