godot_core/meta/
class_name.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
/*
 * Copyright (c) godot-rust; Bromeon and contributors.
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */
use std::any::TypeId;
use std::borrow::Cow;
use std::cell::OnceCell;
use std::collections::HashMap;
use std::ffi::CStr;
use std::fmt;
use std::hash::Hash;

use godot_ffi as sys;
use sys::Global;

use crate::builtin::*;
use crate::obj::GodotClass;

// Alternative optimizations:
// - Small-array optimization for common string lengths.
// - Use HashMap and store pre-computed hash. Would need a custom S parameter for HashMap<K, V, S>, see
//   https://doc.rust-lang.org/std/hash/trait.BuildHasher.html (the default hasher recomputes the hash repeatedly).
//
// First element (index 0) is always the empty string name, which is used for "no class".
static CLASS_NAMES: Global<Vec<ClassNameEntry>> = Global::new(|| vec![ClassNameEntry::none()]);
static DYNAMIC_INDEX_BY_CLASS_TYPE: Global<HashMap<TypeId, u16>> = Global::default();

// ----------------------------------------------------------------------------------------------------------------------------------------------

/// # Safety
/// Must not use any `ClassName` APIs after this call.
pub unsafe fn cleanup() {
    CLASS_NAMES.lock().clear();
    DYNAMIC_INDEX_BY_CLASS_TYPE.lock().clear();
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

/// Entry in the class name cache.
///
/// `StringName` needs to be lazy-initialized because the Godot binding may not be initialized yet.
struct ClassNameEntry {
    rust_str: ClassNameSource,
    godot_str: OnceCell<StringName>,
}

impl ClassNameEntry {
    fn new(rust_str: ClassNameSource) -> Self {
        Self {
            rust_str,
            godot_str: OnceCell::new(),
        }
    }

    fn none() -> Self {
        Self::new(ClassNameSource::Borrowed(c""))
    }
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

/// `Cow`-like enum for class names, but with C strings as the borrowed variant.
enum ClassNameSource {
    Owned(String),
    Borrowed(&'static CStr),
}

impl ClassNameSource {
    fn to_string_name(&self) -> StringName {
        match self {
            ClassNameSource::Owned(s) => StringName::from(s),

            #[cfg(since_api = "4.2")]
            ClassNameSource::Borrowed(cstr) => StringName::from(*cstr),
            #[cfg(before_api = "4.2")] // no C-string support for StringName.
            ClassNameSource::Borrowed(cstr) => StringName::from(ascii_cstr_to_str(cstr)),
        }
    }

    fn as_cow_str(&self) -> Cow<'static, str> {
        match self {
            ClassNameSource::Owned(s) => Cow::Owned(s.clone()),
            ClassNameSource::Borrowed(cstr) => Cow::Borrowed(ascii_cstr_to_str(cstr)),
        }
    }
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

/// Name of a class registered with Godot.
///
/// Holds the Godot name, not the Rust name (they sometimes differ, e.g. Godot `CSGMesh3D` vs Rust `CsgMesh3D`).
///
/// This struct is very cheap to copy. The actual names are cached globally.
///
/// If you need to create your own class name, use [`new_cached()`][Self::new_cached].
///
/// # Ordering
///
/// `ClassName`s are **not** ordered lexicographically, and the ordering relation is **not** stable across multiple runs of your
/// application. When lexicographical order is needed, it's possible to convert this type to [`GString`] or [`String`].
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct ClassName {
    global_index: u16,
}

impl ClassName {
    /// Construct a new class name.
    ///
    /// This is expensive the first time it called for a given `T`, but will be cached for subsequent calls.
    ///
    /// It is not specified when exactly `init_fn` is invoked. However, it must return the same value for the same `T`. Generally, we expect
    /// to keep the invocations limited, so you can use more expensive construction in the closure.
    ///
    /// # Panics
    /// 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.
    pub fn new_cached<T: GodotClass>(init_fn: impl FnOnce() -> String) -> Self {
        // Check if class name exists.
        let type_id = TypeId::of::<T>();
        let mut map = DYNAMIC_INDEX_BY_CLASS_TYPE.lock();

        // Insert into linear vector. Note: this doesn't check for overlaps of TypeId between static and dynamic class names.
        let global_index = *map.entry(type_id).or_insert_with(|| {
            let name = init_fn();

            #[cfg(before_api = "4.4")]
            assert!(
                name.is_ascii(),
                "In Godot < 4.4, class name must be ASCII: '{name}'"
            );

            insert_class(ClassNameSource::Owned(name))
        });

        ClassName { global_index }
    }

    #[doc(hidden)]
    pub fn none() -> Self {
        // First element is always the empty string name.
        Self { global_index: 0 }
    }

    #[doc(hidden)]
    pub fn alloc_next_ascii(class_name_cstr: &'static CStr) -> Self {
        let utf8 = class_name_cstr
            .to_str()
            .expect("class name is invalid UTF-8");

        assert!(
            utf8.is_ascii(),
            "ClassName::alloc_next_ascii() with non-ASCII Unicode string '{}'",
            utf8
        );

        let global_index = insert_class(ClassNameSource::Borrowed(class_name_cstr));

        Self { global_index }
    }

    #[doc(hidden)]
    pub fn alloc_next_unicode(class_name_str: &'static str) -> Self {
        assert!(
            cfg!(since_api = "4.4"),
            "Before Godot 4.4, class names must be ASCII, but '{class_name_str}' is not.\nSee https://github.com/godotengine/godot/pull/96501."
        );

        assert!(
            !class_name_str.is_ascii(),
            "ClassName::alloc_next_unicode() with ASCII string '{}'",
            class_name_str
        );

        // StringNames use optimized 1-byte-per-char layout for Latin-1/ASCII, so Unicode can as well use the regular constructor.
        let global_index = insert_class(ClassNameSource::Owned(class_name_str.to_owned()));

        Self { global_index }
    }

    #[doc(hidden)]
    pub fn is_none(&self) -> bool {
        self.global_index == 0
    }
    //
    // /// Returns the class name as a string slice with static storage duration.
    // pub fn as_str(&self) -> &'static str {
    //     // unwrap() safe, checked in constructor
    //     self.c_str.to_str().unwrap()
    // }

    /// Converts the class name to a `GString`.
    pub fn to_gstring(&self) -> GString {
        self.with_string_name(|s| s.into())
    }

    /// Converts the class name to a `StringName`.
    pub fn to_string_name(&self) -> StringName {
        self.with_string_name(|s| s.clone())
    }

    /// Returns an owned or borrowed `str`.
    pub fn to_cow_str(&self) -> Cow<'static, str> {
        let cached_names = CLASS_NAMES.lock();
        let entry = &cached_names[self.global_index as usize];

        entry.rust_str.as_cow_str()
    }

    /// The returned pointer is valid indefinitely, as entries are never deleted from the cache.
    /// Since we use `Box<StringName>`, `HashMap` reallocations don't affect the validity of the StringName.
    #[doc(hidden)]
    pub fn string_sys(&self) -> sys::GDExtensionConstStringNamePtr {
        self.with_string_name(|s| s.string_sys())
    }

    // Takes a closure because the mutex guard protects the reference; so the &StringName cannot leave the scope.
    fn with_string_name<R>(&self, func: impl FnOnce(&StringName) -> R) -> R {
        let cached_names = CLASS_NAMES.lock();
        let entry = &cached_names[self.global_index as usize];

        let string_name = entry
            .godot_str
            .get_or_init(|| entry.rust_str.to_string_name());

        func(string_name)
    }
}

impl fmt::Display for ClassName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.with_string_name(|s| s.fmt(f))
    }
}

/// Adds a new class name to the cache, returning its index.
fn insert_class(name: ClassNameSource) -> u16 {
    let mut names = CLASS_NAMES.lock();
    let index = names
        .len()
        .try_into()
        .expect("Currently limited to 65536 class names");

    names.push(ClassNameEntry::new(name));
    index
}

fn ascii_cstr_to_str(cstr: &CStr) -> &str {
    cstr.to_str().expect("should be validated ASCII")
}