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 (safeguards-balanced)
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 sys::balanced_assert!(
267 !self.string_to_index.contains_key(source.as_ref()),
268 "insert_class_name() called for already-existing string: {}",
269 source
270 );
271 } else {
272 // Check string cache first (dynamic path may reuse existing entries).
273 if let Some(&existing_index) = self.string_to_index.get(source.as_ref()) {
274 // Update type cache if we have a TypeId and it's not already cached (dynamic-then-static case).
275 // Note: if type_id is Some, we know it came from new_cached_inner after a failed TypeId lookup.
276 if let Some(type_id) = type_id {
277 self.type_to_index.entry(type_id).or_insert(existing_index);
278 }
279 return ClassId {
280 global_index: existing_index,
281 };
282 }
283 }
284
285 // Not found or static path - create new entry.
286 let global_index =
287 self.entries.len().try_into().unwrap_or_else(|_| {
288 panic!("ClassId cache exceeded maximum capacity of 65536 entries")
289 });
290
291 self.entries.push(ClassIdEntry::new(source.clone()));
292 self.string_to_index
293 .insert(source.into_owned(), global_index);
294
295 if let Some(type_id) = type_id {
296 self.type_to_index.insert(type_id, global_index);
297 }
298
299 ClassId { global_index }
300 }
301
302 fn get_by_type_id(&self, type_id: TypeId) -> Option<u16> {
303 self.type_to_index.get(&type_id).copied()
304 }
305
306 fn get_entry(&self, index: usize) -> &ClassIdEntry {
307 &self.entries[index]
308 }
309
310 fn clear(&mut self) {
311 // MACOS-PARTIAL-RELOAD: Previous implementation for when upstream fixes `.gdextension` reload.
312 // self.entries.clear();
313 // self.type_to_index.clear();
314 // self.string_to_index.clear();
315
316 // MACOS-PARTIAL-RELOAD: Preserve existing `ClassId` entries when only the `.gdextension` reloads so indices stay valid.
317 // There are two types of hot reload: `dylib` reload (`dylib` `mtime` newer) unloads and reloads the library, whereas
318 // `.gdextension` reload (`.gdextension` `mtime` newer) re-initializes the existing `dylib` without unloading it. To handle
319 // `.gdextension` reload, keep the backing entries (and thus the `string_to_index` map) but drop cached Godot `StringNames`
320 // and the `TypeId` lookup so they can be rebuilt.
321 for entry in &mut self.entries {
322 entry.godot_str = OnceCell::new();
323 }
324
325 self.type_to_index.clear();
326 }
327}