Skip to main content

uika_runtime/
reify_registry.rs

1// Reify registry: manages Rust-side type info, function callbacks, and instance data
2// for runtime-created UE classes.
3//
4// Three registries:
5// 1. Type registry: maps type_id -> RustTypeInfo (constructor, destructor, name)
6// 2. Function registry: maps callback_id -> Rust function closure
7// 3. Instance data: maps UObject pointer -> allocated Rust data
8
9use std::collections::HashMap;
10use std::sync::{Arc, Mutex, OnceLock, RwLock};
11
12// ---------------------------------------------------------------------------
13// Inventory-based auto-registration
14// ---------------------------------------------------------------------------
15
16/// Submitted by `#[uclass]` — holds register + finalize fn pointers.
17pub struct ClassRegistration {
18    pub register: fn(&uika_ffi::UikaApiTable),
19    pub finalize: fn(&uika_ffi::UikaApiTable),
20}
21inventory::collect!(ClassRegistration);
22
23/// Submitted by `#[uclass_impl]` — holds register_functions fn pointer.
24pub struct ClassFunctionRegistration {
25    pub register_functions: fn(&uika_ffi::UikaApiTable),
26}
27inventory::collect!(ClassFunctionRegistration);
28
29/// Three-phase iteration: register all → register all functions → finalize all.
30pub fn register_all_from_inventory(table: &uika_ffi::UikaApiTable) {
31    let mut class_count = 0u32;
32    for reg in inventory::iter::<ClassRegistration> {
33        (reg.register)(table);
34        class_count += 1;
35    }
36    let mut func_reg_count = 0u32;
37    for freg in inventory::iter::<ClassFunctionRegistration> {
38        (freg.register_functions)(table);
39        func_reg_count += 1;
40    }
41    for reg in inventory::iter::<ClassRegistration> {
42        (reg.finalize)(table);
43    }
44
45    // Log registration summary (helps diagnose hot-reload issues).
46    if !table.logging.is_null() {
47        let total_funcs = func_registry().read().unwrap().len();
48        let msg = format!(
49            "[Uika] register_all_from_inventory: {} classes, {} impl blocks, {} function callbacks",
50            class_count, func_reg_count, total_funcs,
51        );
52        let bytes = msg.as_bytes();
53        unsafe {
54            ((*table.logging).log)(0, bytes.as_ptr(), bytes.len() as u32);
55        }
56    }
57}
58
59use uika_ffi::UObjectHandle;
60
61/// Information about a Rust type registered for reification.
62pub struct RustTypeInfo {
63    /// Human-readable type name (for debugging).
64    pub name: &'static str,
65    /// Allocate and return a default-initialized instance. The returned pointer
66    /// must be freeable by `drop_fn`.
67    pub construct_fn: fn() -> *mut u8,
68    /// Drop and deallocate an instance previously created by `construct_fn`.
69    pub drop_fn: unsafe fn(*mut u8),
70}
71
72// Type for reify function callbacks: (obj, rust_data, params)
73// Uses Arc so we can clone the reference out of the registry and release
74// the lock before invoking the callback (prevents deadlock if the callback
75// makes FFI calls that re-enter Rust).
76type ReifyFunctionCallback = Arc<dyn Fn(UObjectHandle, *mut u8, *mut u8) + Send + Sync>;
77
78// ---------------------------------------------------------------------------
79// Statics
80// ---------------------------------------------------------------------------
81
82static TYPE_REGISTRY: OnceLock<Mutex<HashMap<u64, RustTypeInfo>>> = OnceLock::new();
83static FUNC_REGISTRY: OnceLock<RwLock<Vec<ReifyFunctionCallback>>> = OnceLock::new();
84static INSTANCE_DATA: OnceLock<RwLock<HashMap<usize, InstanceEntry>>> = OnceLock::new();
85
86struct InstanceEntry {
87    data: *mut u8,
88    type_id: u64,
89}
90
91// SAFETY: The raw pointer in InstanceEntry is only accessed on the game thread.
92unsafe impl Send for InstanceEntry {}
93unsafe impl Sync for InstanceEntry {}
94
95fn type_registry() -> &'static Mutex<HashMap<u64, RustTypeInfo>> {
96    TYPE_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
97}
98
99fn func_registry() -> &'static RwLock<Vec<ReifyFunctionCallback>> {
100    FUNC_REGISTRY.get_or_init(|| RwLock::new(Vec::new()))
101}
102
103fn instance_data() -> &'static RwLock<HashMap<usize, InstanceEntry>> {
104    INSTANCE_DATA.get_or_init(|| RwLock::new(HashMap::new()))
105}
106
107// ---------------------------------------------------------------------------
108// Type registry
109// ---------------------------------------------------------------------------
110
111/// Register a Rust type for reification. Must be called before `create_class`.
112pub fn register_type(type_id: u64, info: RustTypeInfo) {
113    type_registry()
114        .lock()
115        .unwrap()
116        .insert(type_id, info);
117}
118
119// ---------------------------------------------------------------------------
120// Function registry
121// ---------------------------------------------------------------------------
122
123/// Register a Rust function callback and return its unique callback ID.
124pub fn register_function<F>(f: F) -> u64
125where
126    F: Fn(UObjectHandle, *mut u8, *mut u8) + Send + Sync + 'static,
127{
128    let mut vec = func_registry().write().unwrap();
129    let id = vec.len() as u64;
130    vec.push(Arc::new(f));
131    id
132}
133
134// ---------------------------------------------------------------------------
135// Instance lifecycle
136// ---------------------------------------------------------------------------
137
138/// Construct a Rust instance for a newly created UObject.
139/// Called from the C++ class constructor via `construct_rust_instance` callback.
140pub fn construct_instance(obj: UObjectHandle, type_id: u64) {
141    let types = type_registry().lock().unwrap();
142    let Some(info) = types.get(&type_id) else {
143        // Log warning — type not registered (might be a CDO before registration completes)
144        if crate::api::is_api_initialized() {
145            let msg = format!("[Uika] construct_instance: unknown type_id {type_id}");
146            let bytes = msg.as_bytes();
147            unsafe {
148                let api = crate::api::api();
149                ((*api.logging).log)(1, bytes.as_ptr(), bytes.len() as u32);
150            }
151        }
152        return;
153    };
154    let data = (info.construct_fn)();
155    drop(types); // Release lock before acquiring instance_data lock
156
157    let key = obj.0 as usize;
158    instance_data()
159        .write()
160        .unwrap()
161        .insert(key, InstanceEntry { data, type_id });
162}
163
164/// Drop and remove the Rust instance for a destroyed UObject.
165/// Called from the C++ delete listener via `drop_rust_instance` callback.
166pub fn drop_instance(obj: UObjectHandle, _type_id: u64) {
167    let key = obj.0 as usize;
168    let entry = instance_data().write().unwrap().remove(&key);
169
170    if let Some(entry) = entry {
171        let types = type_registry().lock().unwrap();
172        if let Some(info) = types.get(&entry.type_id) {
173            unsafe {
174                (info.drop_fn)(entry.data);
175            }
176        }
177    }
178}
179
180/// Invoke a registered Rust function callback.
181/// Called from the C++ thunk via `invoke_rust_function` callback.
182pub fn invoke_function(callback_id: u64, obj: UObjectHandle, params: *mut u8) {
183    let key = obj.0 as usize;
184
185    // Look up instance data for this object (read lock — non-exclusive).
186    let rust_data = instance_data()
187        .read()
188        .unwrap()
189        .get(&key)
190        .map(|e| e.data)
191        .unwrap_or(std::ptr::null_mut());
192
193    // Clone the callback Arc out of the registry and release the read lock
194    // BEFORE invoking the callback. This prevents deadlocks if the callback
195    // makes FFI calls that re-enter Rust.
196    let func = {
197        let vec = func_registry().read().unwrap();
198        vec.get(callback_id as usize).cloned()
199    };
200
201    if let Some(func) = func {
202        func(obj, rust_data, params);
203    } else if crate::api::is_api_initialized() {
204        let vec_len = func_registry().read().unwrap().len();
205        let msg = format!(
206            "[Uika] invoke_function: callback_id {} not found (registry size = {})",
207            callback_id, vec_len,
208        );
209        let bytes = msg.as_bytes();
210        unsafe {
211            let api = crate::api::api();
212            ((*api.logging).log)(1, bytes.as_ptr(), bytes.len() as u32);
213        }
214    }
215}
216
217/// Clear all registries and drop all instance data.
218/// Called during shutdown before DLL unload (enables hot reload).
219pub fn clear_all() {
220    // 1. Drop all instance data, using the type's drop_fn.
221    if let Some(instances) = INSTANCE_DATA.get() {
222        let mut map = instances.write().unwrap();
223        let types = type_registry().lock().unwrap();
224        for (_, entry) in map.drain() {
225            if let Some(info) = types.get(&entry.type_id) {
226                unsafe {
227                    (info.drop_fn)(entry.data);
228                }
229            }
230        }
231        drop(types);
232    }
233    // 2. Clear function registry.
234    if let Some(funcs) = FUNC_REGISTRY.get() {
235        funcs.write().unwrap().clear();
236    }
237    // 3. Clear type registry.
238    if let Some(types) = TYPE_REGISTRY.get() {
239        types.lock().unwrap().clear();
240    }
241}
242
243/// Get the Rust instance data pointer for a UObject.
244/// Returns null if no instance data is registered.
245pub fn get_instance_data(obj: UObjectHandle) -> *mut u8 {
246    let key = obj.0 as usize;
247    instance_data()
248        .read()
249        .unwrap()
250        .get(&key)
251        .map(|e| e.data)
252        .unwrap_or(std::ptr::null_mut())
253}