intuicio_data/type_hash.rs
1//! Cheap runtime type identity.
2//!
3//! See [`TypeHash`].
4use rustc_hash::FxHasher;
5use std::{
6 cmp::Ordering,
7 hash::{Hash, Hasher},
8};
9
10/// Runtime identity of a type, stored as a hash of its full type name.
11///
12/// Used instead of [`std::any::TypeId`] because it can also be built for
13/// types that only exist on the script side, from their name alone (see
14/// [`TypeHash::raw`]). Comparison, ordering and hashing all use the hash
15/// only, never the name.
16///
17/// ```
18/// # use intuicio_data::type_hash::TypeHash;
19/// assert_eq!(TypeHash::of::<i32>(), TypeHash::of::<i32>());
20/// assert_ne!(TypeHash::of::<i32>(), TypeHash::of::<f32>());
21/// ```
22#[derive(Debug, Copy, Clone)]
23pub struct TypeHash {
24 hash: u64,
25 #[cfg(feature = "typehash_debug_name")]
26 name: Option<&'static str>,
27}
28
29impl Default for TypeHash {
30 fn default() -> Self {
31 Self::INVALID
32 }
33}
34
35impl TypeHash {
36 /// Hash that no real type maps to, used as a null value.
37 ///
38 /// This is also what [`TypeHash::default`] returns.
39 pub const INVALID: Self = Self {
40 hash: 0,
41 #[cfg(feature = "typehash_debug_name")]
42 name: None,
43 };
44
45 /// Builds a hash from a type name given at runtime.
46 ///
47 /// # Safety
48 ///
49 /// The name must be the full, qualified name of the type, the same name
50 /// [`TypeHash::of`] builds its hash from. A different name makes two views
51 /// of one type compare as different types. Type-erased containers use that
52 /// comparison to decide if a cast is safe.
53 pub unsafe fn raw(name: &str) -> Self {
54 let mut hasher = FxHasher::default();
55 name.hash(&mut hasher);
56 Self {
57 hash: hasher.finish(),
58 #[cfg(feature = "typehash_debug_name")]
59 name: None,
60 }
61 }
62
63 /// Same as [`TypeHash::raw`], but keeps the name for diagnostics when the
64 /// `typehash_debug_name` feature is on.
65 ///
66 /// # Safety
67 ///
68 /// Same as [`TypeHash::raw`].
69 pub unsafe fn raw_static(name: &'static str) -> Self {
70 let mut hasher = FxHasher::default();
71 name.hash(&mut hasher);
72 Self {
73 hash: hasher.finish(),
74 #[cfg(feature = "typehash_debug_name")]
75 name: Some(name),
76 }
77 }
78
79 /// Builds the hash of a type that exists only on the script side, from its
80 /// qualified name.
81 ///
82 /// The name is salted with NUL bytes, which [`std::any::type_name`] never
83 /// produces. So a runtime type never gets the same hash as a Rust type, and
84 /// a runtime value never passes the check that reads it as a Rust type.
85 /// Reflection stays the only way to read such a value.
86 ///
87 /// This is safe, unlike [`TypeHash::raw`], because the hash it builds
88 /// cannot match a Rust type.
89 ///
90 /// ```
91 /// # use intuicio_data::type_hash::TypeHash;
92 /// assert_eq!(
93 /// TypeHash::of_runtime("game::Player"),
94 /// TypeHash::of_runtime("game::Player")
95 /// );
96 /// assert_ne!(
97 /// TypeHash::of_runtime("game::Player"),
98 /// TypeHash::of_runtime("game::Vector")
99 /// );
100 /// assert_ne!(TypeHash::of_runtime("i32"), TypeHash::of::<i32>());
101 /// ```
102 pub fn of_runtime(qualified_name: &str) -> Self {
103 const SALT: &str = "\0intuicio::runtime\0";
104 let mut hasher = FxHasher::default();
105 SALT.hash(&mut hasher);
106 qualified_name.hash(&mut hasher);
107 Self {
108 hash: hasher.finish(),
109 #[cfg(feature = "typehash_debug_name")]
110 name: None,
111 }
112 }
113
114 /// Builds the hash of a Rust type known at compile time.
115 pub fn of<T: ?Sized>() -> Self {
116 let name = std::any::type_name::<T>();
117 let mut hasher = FxHasher::default();
118 name.hash(&mut hasher);
119 Self {
120 hash: hasher.finish(),
121 #[cfg(feature = "typehash_debug_name")]
122 name: Some(name),
123 }
124 }
125
126 /// Returns `false` only for [`TypeHash::INVALID`].
127 pub fn is_valid(&self) -> bool {
128 self.hash != Self::INVALID.hash
129 }
130
131 /// Returns the raw hash value.
132 pub fn hash(&self) -> u64 {
133 self.hash
134 }
135
136 /// Returns the type name this hash was built from, when it is known.
137 ///
138 /// Only available with the `typehash_debug_name` feature.
139 #[cfg(feature = "typehash_debug_name")]
140 pub fn name(&self) -> Option<&'static str> {
141 self.name
142 }
143}
144
145impl PartialEq for TypeHash {
146 fn eq(&self, other: &Self) -> bool {
147 self.hash == other.hash
148 }
149}
150
151impl Eq for TypeHash {}
152
153impl PartialOrd for TypeHash {
154 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155 Some(self.cmp(other))
156 }
157}
158
159impl Ord for TypeHash {
160 fn cmp(&self, other: &Self) -> Ordering {
161 self.hash.cmp(&other.hash)
162 }
163}
164
165impl Hash for TypeHash {
166 fn hash<H: Hasher>(&self, state: &mut H) {
167 self.hash.hash(state);
168 }
169}
170
171impl std::fmt::Display for TypeHash {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 #[cfg(feature = "typehash_debug_name")]
174 {
175 if let Some(name) = self.name {
176 return write!(f, "#{:X}: {}", self.hash, name);
177 }
178 }
179 write!(f, "#{:X}", self.hash)
180 }
181}