Skip to main content

steel_utils/
downcast.rs

1//! Deterministic concrete-type downcasting for erased Steel objects.
2
3use std::fmt::{self, Display, Formatter};
4use std::ptr::{from_mut, from_ref};
5
6/// A process-wide key identifying one concrete Rust type for downcasting.
7///
8/// Keys conventionally use `<owner>:<kind>/<name>`, such as
9/// `steel:entity/item`. They identify Rust implementations, not Minecraft
10/// registry entries or translation keys.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct DowncastTypeKey(&'static str);
13
14impl DowncastTypeKey {
15    /// Creates a non-empty downcast type key.
16    ///
17    /// # Panics
18    ///
19    /// Panics if `key` is empty.
20    #[must_use]
21    pub const fn new(key: &'static str) -> Self {
22        assert!(!key.is_empty(), "downcast type keys cannot be empty");
23        Self(key)
24    }
25
26    /// Returns the key as a string.
27    #[must_use]
28    pub const fn as_str(self) -> &'static str {
29        self.0
30    }
31}
32
33impl Display for DowncastTypeKey {
34    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
35        formatter.write_str(self.0)
36    }
37}
38
39/// Assigns a deterministic downcast key to a concrete Rust type.
40///
41/// This trait does not assert that the type has a stable ABI. A plugin may use
42/// it to recover types that plugin owns, but a key alone never permits one
43/// compilation unit to reinterpret another compilation unit's private type.
44///
45/// # Safety
46///
47/// Implementors must guarantee that:
48///
49/// - `TYPE_KEY` uniquely identifies this exact concrete Rust type among all
50///   [`DowncastType`] implementations that may coexist in the process.
51/// - No separately defined type uses the same key, even if it currently has an
52///   identical layout.
53/// - Versions of a type that may coexist use different keys unless they are the
54///   same concrete Rust type to every caller that can perform the downcast.
55pub unsafe trait DowncastType: 'static {
56    /// The process-wide key for this concrete type.
57    const TYPE_KEY: DowncastTypeKey;
58}
59
60macro_rules! impl_steel_downcast_type {
61    ($type:ty, $key:literal) => {
62        // SAFETY: This Steel-owned key uniquely identifies the concrete Rust
63        // type within the process.
64        unsafe impl DowncastType for $type {
65            const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new($key);
66        }
67    };
68}
69
70impl_steel_downcast_type!((), "steel:rust_primitive/unit");
71impl_steel_downcast_type!(bool, "steel:rust_primitive/bool");
72impl_steel_downcast_type!(i32, "steel:rust_primitive/i32");
73impl_steel_downcast_type!(f32, "steel:rust_primitive/f32");
74impl_steel_downcast_type!(crate::Identifier, "steel:value/identifier");
75impl_steel_downcast_type!(text_components::TextComponent, "steel:value/text_component");
76
77mod private {
78    pub trait Sealed {}
79
80    impl<T: super::DowncastType> Sealed for T {}
81}
82
83/// Object-safe type erasure implemented for every [`DowncastType`].
84///
85/// This trait is sealed so erased pointers can only be produced by the blanket
86/// implementation below.
87pub trait ErasedType: private::Sealed {
88    /// Returns the concrete type's deterministic key.
89    #[doc(hidden)]
90    fn downcast_type_key(&self) -> DowncastTypeKey;
91
92    /// Returns a pointer to the concrete value.
93    #[doc(hidden)]
94    fn downcast_data(&self) -> *const ();
95
96    /// Returns a mutable pointer to the concrete value.
97    #[doc(hidden)]
98    fn downcast_data_mut(&mut self) -> *mut ();
99}
100
101impl<T: DowncastType> ErasedType for T {
102    fn downcast_type_key(&self) -> DowncastTypeKey {
103        T::TYPE_KEY
104    }
105
106    fn downcast_data(&self) -> *const () {
107        from_ref(self).cast()
108    }
109
110    fn downcast_data_mut(&mut self) -> *mut () {
111        from_mut(self).cast()
112    }
113}
114
115/// Extension methods for deterministic concrete-type downcasting.
116pub trait Downcast: ErasedType {
117    /// Returns whether the erased value has concrete type `T`.
118    #[must_use]
119    fn is<T: DowncastType>(&self) -> bool {
120        self.downcast_type_key() == T::TYPE_KEY
121    }
122
123    /// Returns the erased value as `T` when its key matches.
124    #[must_use]
125    fn downcast_ref<T: DowncastType>(&self) -> Option<&T> {
126        if !self.is::<T>() {
127            return None;
128        }
129
130        // SAFETY: `DowncastType` requires equal keys to identify the exact same
131        // concrete Rust type, and `ErasedType` is sealed so its pointer always
132        // points to the concrete implementor.
133        Some(unsafe { &*self.downcast_data().cast::<T>() })
134    }
135
136    /// Returns the erased value as mutable `T` when its key matches.
137    #[must_use]
138    fn downcast_mut<T: DowncastType>(&mut self) -> Option<&mut T> {
139        if !self.is::<T>() {
140            return None;
141        }
142
143        // SAFETY: `DowncastType` requires equal keys to identify the exact same
144        // concrete Rust type, and the exclusive borrow guarantees that the
145        // erased pointer can be reborrowed mutably for the returned lifetime.
146        Some(unsafe { &mut *self.downcast_data_mut().cast::<T>() })
147    }
148}
149
150impl<T: ErasedType + ?Sized> Downcast for T {}
151
152#[cfg(test)]
153mod tests {
154    use super::{Downcast as _, DowncastType, DowncastTypeKey, ErasedType};
155
156    struct First(u32);
157    struct Second;
158
159    // SAFETY: These test-only keys are distinct and identify their respective
160    // concrete types within the test process.
161    unsafe impl DowncastType for First {
162        const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:test/downcast/first");
163    }
164
165    // SAFETY: These test-only keys are distinct and identify their respective
166    // concrete types within the test process.
167    unsafe impl DowncastType for Second {
168        const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:test/downcast/second");
169    }
170
171    #[test]
172    fn downcasts_matching_shared_reference() {
173        let value = First(7);
174        let erased: &dyn ErasedType = &value;
175
176        assert!(erased.is::<First>());
177        assert!(!erased.is::<Second>());
178        assert_eq!(erased.downcast_ref::<First>().map(|first| first.0), Some(7));
179        assert!(erased.downcast_ref::<Second>().is_none());
180    }
181
182    #[test]
183    fn downcasts_matching_mutable_reference() {
184        let mut value = First(7);
185        let erased: &mut dyn ErasedType = &mut value;
186        let Some(first) = erased.downcast_mut::<First>() else {
187            panic!("matching type key should downcast");
188        };
189        first.0 = 11;
190
191        assert_eq!(value.0, 11);
192    }
193}