uika_runtime/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::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, Mutex, OnceLock};
12
13use uika_ffi::UObjectHandle;
14
15use crate::api::api;
16use crate::error::{UikaError, UikaResult};
17use crate::object_ref::{Checked, UObjectRef};
18use crate::traits::{UeClass, UeHandle, ValidHandle};
19
20// ---------------------------------------------------------------------------
21// Alive registry — maps UObject pointer → alive flag for fast checked_handle
22// ---------------------------------------------------------------------------
23
24fn alive_registry() -> &'static Mutex<HashMap<usize, Arc<AtomicBool>>> {
25 static REGISTRY: OnceLock<Mutex<HashMap<usize, Arc<AtomicBool>>>> = OnceLock::new();
26 REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
27}
28
29/// Called from C++ (via FUikaRustCallbacks) when a pinned object is destroyed
30/// by DestroyActor, level unload, PIE end, etc. Sets the alive flag to false
31/// so subsequent `checked_handle()` calls return `Err(ObjectDestroyed)`.
32pub fn notify_pinned_destroyed(handle: UObjectHandle) {
33 if let Ok(registry) = alive_registry().lock() {
34 if let Some(flag) = registry.get(&(handle.0 as usize)) {
35 flag.store(false, Ordering::Relaxed);
36 }
37 }
38}
39
40/// Clear all alive flags. Called during on_shutdown (hot reload / DLL unload).
41pub fn clear_all() {
42 if let Ok(mut registry) = alive_registry().lock() {
43 registry.clear();
44 }
45}
46
47// ---------------------------------------------------------------------------
48// Pinned<T>
49// ---------------------------------------------------------------------------
50
51/// An owning GC root for a UObject.
52///
53/// - `!Copy`, `!Clone` — unique ownership of the GC root.
54/// - `Send` — can be moved across threads.
55/// - `!Sync` — must only be *used* on the game thread.
56/// - `Drop` removes the GC root and unregisters from destroy notification.
57///
58/// Method calls on `Pinned<T>` use a local alive flag (~1-3 cycles) instead
59/// of an FFI `is_valid` call (~15-30 cycles) for validity checking.
60pub struct Pinned<T: UeClass> {
61 handle: UObjectHandle,
62 alive: Arc<AtomicBool>,
63 _marker: PhantomData<*const T>, // !Sync
64}
65
66unsafe impl<T: UeClass> Send for Pinned<T> {}
67
68impl<T: UeClass> Pinned<T> {
69 /// Pin an object by adding a GC root and registering for destroy notification.
70 /// Fails if the object is already destroyed.
71 pub fn new(obj: UObjectRef<T>) -> UikaResult<Self> {
72 if !obj.is_valid() {
73 return Err(UikaError::ObjectDestroyed);
74 }
75 let alive = Arc::new(AtomicBool::new(true));
76 // Register in alive registry (for C++ destroy notification → Rust alive flag).
77 alive_registry().lock().unwrap()
78 .insert(obj.raw().0 as usize, alive.clone());
79 // GC root + destroy notification registration.
80 unsafe {
81 ((*api().lifecycle).add_gc_root)(obj.raw());
82 ((*api().lifecycle).register_pinned)(obj.raw());
83 }
84 Ok(Pinned {
85 handle: obj.raw(),
86 alive,
87 _marker: PhantomData,
88 })
89 }
90
91 /// Check whether the pinned object is still alive (local memory read).
92 #[inline]
93 pub fn is_alive(&self) -> bool {
94 self.alive.load(Ordering::Relaxed)
95 }
96
97 /// Get the underlying raw handle. Guaranteed valid while this `Pinned`
98 /// exists and `is_alive()` returns true.
99 #[inline]
100 pub fn handle(&self) -> UObjectHandle {
101 self.handle
102 }
103
104 /// Borrow as a lightweight `UObjectRef`. The returned ref is valid as long
105 /// as this `Pinned` is alive.
106 #[inline]
107 pub fn as_ref(&self) -> UObjectRef<T> {
108 // SAFETY: The GC root guarantees the object is alive, and we know
109 // the type is correct because it was validated at construction.
110 unsafe { UObjectRef::from_raw(self.handle) }
111 }
112
113 /// Create a `Checked<T>` handle from this pinned reference.
114 /// Valid as long as `is_alive()` returns true (debug-asserted).
115 #[inline]
116 pub fn as_checked(&self) -> Checked<T> {
117 debug_assert!(self.is_alive(), "Pinned object has been destroyed");
118 Checked::new_unchecked(self.handle)
119 }
120}
121
122impl<T: UeClass> Drop for Pinned<T> {
123 fn drop(&mut self) {
124 // Remove from alive registry.
125 alive_registry().lock().unwrap().remove(&(self.handle.0 as usize));
126 // Unregister from C++ destroy notification, then remove GC root.
127 unsafe {
128 ((*api().lifecycle).unregister_pinned)(self.handle);
129 ((*api().lifecycle).remove_gc_root)(self.handle);
130 }
131 }
132}
133
134impl<T: UeClass> ValidHandle for Pinned<T> {
135 #[inline]
136 fn handle(&self) -> UObjectHandle {
137 debug_assert!(self.is_alive(), "Pinned object has been destroyed");
138 self.handle
139 }
140}
141
142impl<T: UeClass> UeHandle for Pinned<T> {
143 #[inline]
144 fn checked_handle(&self) -> UikaResult<UObjectHandle> {
145 // Local memory read (~1-3 cycles) instead of FFI is_valid (~15-30 cycles).
146 if self.alive.load(Ordering::Relaxed) {
147 Ok(self.handle)
148 } else {
149 Err(UikaError::ObjectDestroyed)
150 }
151 }
152
153 #[inline]
154 fn raw_handle(&self) -> UObjectHandle {
155 self.handle
156 }
157}
158
159impl<T: UeClass> std::fmt::Debug for Pinned<T> {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 f.debug_struct("Pinned")
162 .field("handle", &self.handle)
163 .field("alive", &self.is_alive())
164 .finish()
165 }
166}