rusteal_core/pinned.rs
1// Pinned<T>: RAII GC root that keeps a UObject alive until dropped.
2//
3// Construction calls add_gc_root + register_pinned; Drop calls unregister_pinned
4// + remove_gc_root. The GC root prevents garbage collection, while the pinned
5// registration enables fast alive-flag checking via a local AtomicBool instead
6// of an FFI is_valid call on every method invocation.
7
8use std::collections::HashMap;
9use std::marker::PhantomData;
10use std::ops::Deref;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::{Arc, Mutex, OnceLock};
13
14use crate::lock_or_recover;
15
16use rusteal_ffi::UObjectHandle;
17
18use crate::error::{RustealError, RustealResult};
19use crate::ffi_dispatch;
20use crate::object_ref::{Checked, UObjectRef};
21use crate::traits::{HasParent, UeClass, UeHandle, ValidHandle};
22
23// ---------------------------------------------------------------------------
24// Alive registry — maps UObject pointer → alive flag for fast checked_handle
25// ---------------------------------------------------------------------------
26
27fn alive_registry() -> &'static Mutex<HashMap<u64, Arc<AtomicBool>>> {
28 static REGISTRY: OnceLock<Mutex<HashMap<u64, Arc<AtomicBool>>>> = OnceLock::new();
29 REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
30}
31
32/// Called from C++ (via FRustealRustCallbacks) when a pinned object is destroyed
33/// by DestroyActor, level unload, PIE end, etc. Sets the alive flag to false
34/// so subsequent `checked_handle()` calls return `Err(ObjectDestroyed)`.
35pub fn notify_pinned_destroyed(handle: UObjectHandle) {
36 if let Ok(registry) = alive_registry().lock()
37 && let Some(flag) = registry.get(&handle.to_addr())
38 {
39 flag.store(false, Ordering::Relaxed);
40 }
41}
42
43/// Clear all alive flags. Called during on_shutdown (hot reload / DLL unload).
44pub fn clear_all() {
45 if let Ok(mut registry) = alive_registry().lock() {
46 registry.clear();
47 }
48}
49
50// ---------------------------------------------------------------------------
51// Pinned<T>
52// ---------------------------------------------------------------------------
53
54/// An owning GC root for a UObject.
55///
56/// - `!Copy`, `!Clone` — unique ownership of the GC root.
57/// - `Send` — can be moved across threads.
58/// - `!Sync` — must only be *used* on the game thread.
59/// - `Drop` removes the GC root and unregisters from destroy notification.
60///
61/// Method calls on `Pinned<T>` use a local alive flag (~1-3 cycles) instead
62/// of an FFI `is_valid` call (~15-30 cycles) for validity checking.
63///
64/// `#[repr(C)]` guarantees deterministic layout so that the blanket Deref
65/// pointer cast (`&Pinned<T>` → `&Pinned<T::Parent>`) is sound.
66#[repr(C)]
67pub struct Pinned<T: UeClass> {
68 handle: UObjectHandle,
69 alive: Arc<AtomicBool>,
70 _marker: PhantomData<*const T>, // !Sync
71}
72
73unsafe impl<T: UeClass> Send for Pinned<T> {}
74
75impl<T: UeClass> Pinned<T> {
76 /// Pin an object by adding a GC root and registering for destroy notification.
77 /// Fails if the object is already destroyed.
78 pub fn new(obj: UObjectRef<T>) -> RustealResult<Self> {
79 if !obj.is_valid() {
80 return Err(RustealError::ObjectDestroyed);
81 }
82 let alive = Arc::new(AtomicBool::new(true));
83 // Register in alive registry (for C++ destroy notification → Rust alive flag).
84 lock_or_recover(alive_registry())
85 .insert(obj.raw().to_addr(), alive.clone());
86 // GC root + destroy notification registration.
87 unsafe {
88 ffi_dispatch::lifecycle_add_gc_root(obj.raw());
89 ffi_dispatch::lifecycle_register_pinned(obj.raw());
90 }
91 Ok(Pinned {
92 handle: obj.raw(),
93 alive,
94 _marker: PhantomData,
95 })
96 }
97
98 /// Check whether the pinned object is still alive (local memory read).
99 #[inline]
100 pub fn is_alive(&self) -> bool {
101 self.alive.load(Ordering::Relaxed)
102 }
103
104 /// Get the underlying raw handle. Guaranteed valid while this `Pinned`
105 /// exists and `is_alive()` returns true.
106 #[inline]
107 pub fn handle(&self) -> UObjectHandle {
108 self.handle
109 }
110
111 /// Borrow as a lightweight `UObjectRef`. The returned ref is valid as long
112 /// as this `Pinned` is alive.
113 #[inline]
114 pub fn as_ref(&self) -> UObjectRef<T> {
115 // SAFETY: The GC root guarantees the object is alive, and we know
116 // the type is correct because it was validated at construction.
117 unsafe { UObjectRef::from_raw(self.handle) }
118 }
119
120 /// Create a `Checked<T>` handle from this pinned reference.
121 /// Valid as long as `is_alive()` returns true (debug-asserted).
122 #[inline]
123 pub fn as_checked(&self) -> Checked<T> {
124 debug_assert!(self.is_alive(), "Pinned object has been destroyed");
125 Checked::new_unchecked(self.handle)
126 }
127}
128
129impl<T: UeClass> Drop for Pinned<T> {
130 fn drop(&mut self) {
131 // Remove from alive registry.
132 lock_or_recover(alive_registry()).remove(&self.handle.to_addr());
133 // Unregister from C++ destroy notification, then remove GC root.
134 unsafe {
135 ffi_dispatch::lifecycle_unregister_pinned(self.handle);
136 ffi_dispatch::lifecycle_remove_gc_root(self.handle);
137 }
138 }
139}
140
141impl<T: UeClass> ValidHandle for Pinned<T> {
142 #[inline]
143 fn handle(&self) -> UObjectHandle {
144 debug_assert!(self.is_alive(), "Pinned object has been destroyed");
145 self.handle
146 }
147}
148
149impl<T: UeClass> UeHandle for Pinned<T> {
150 #[inline]
151 fn checked_handle(&self) -> RustealResult<UObjectHandle> {
152 // Local memory read (~1-3 cycles) instead of FFI is_valid (~15-30 cycles).
153 if self.alive.load(Ordering::Relaxed) {
154 Ok(self.handle)
155 } else {
156 Err(RustealError::ObjectDestroyed)
157 }
158 }
159
160 #[inline]
161 fn raw_handle(&self) -> UObjectHandle {
162 self.handle
163 }
164}
165
166impl<T: UeClass> std::fmt::Debug for Pinned<T> {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 f.debug_struct("Pinned")
169 .field("handle", &self.handle)
170 .field("alive", &self.is_alive())
171 .finish()
172 }
173}
174
175/// Blanket Deref: `Pinned<Child>` auto-derefs to `Pinned<Parent>`.
176/// Safe because `Pinned<T>` is `#[repr(C)]` and `PhantomData` is zero-sized,
177/// so `Pinned<T>` and `Pinned<T::Parent>` have identical layout.
178///
179/// No `upcast()` on Pinned — it owns a GC root, so consuming would be unsound.
180/// Use Deref for borrowed access to parent methods instead.
181impl<T: HasParent> Deref for Pinned<T> {
182 type Target = Pinned<T::Parent>;
183 #[inline]
184 fn deref(&self) -> &Pinned<T::Parent> {
185 unsafe { &*(self as *const _ as *const Pinned<T::Parent>) }
186 }
187}