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")
}