Skip to main content

uika_runtime/
delegate_registry.rs

1// Delegate callback registry: maps callback IDs to Rust closures.
2// When UE fires a delegate, the C++ proxy calls invoke_delegate_callback(id, params),
3// which looks up and calls the registered closure.
4
5use std::collections::HashMap;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Mutex, OnceLock};
8
9use uika_ffi::{FPropertyHandle, UObjectHandle, UikaErrorCode};
10
11use crate::error::{check_ffi, UikaResult};
12
13type DelegateCallback = Option<Box<dyn FnMut(*mut u8) + Send>>;
14
15static NEXT_ID: AtomicU64 = AtomicU64::new(1);
16static REGISTRY: OnceLock<Mutex<HashMap<u64, DelegateCallback>>> = OnceLock::new();
17
18fn registry() -> &'static Mutex<HashMap<u64, DelegateCallback>> {
19    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
20}
21
22/// Register a closure and return its unique callback ID.
23pub fn register_callback(f: impl FnMut(*mut u8) + Send + 'static) -> u64 {
24    let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
25    registry().lock().unwrap().insert(id, Some(Box::new(f)));
26    id
27}
28
29/// Unregister a callback by its ID.
30pub fn unregister_callback(id: u64) {
31    registry().lock().unwrap().remove(&id);
32}
33
34/// Clear all callbacks and reset the ID counter.
35/// Called during shutdown before DLL unload (enables hot reload).
36pub fn clear_all() {
37    if let Some(reg) = REGISTRY.get() {
38        reg.lock().unwrap().clear();
39    }
40    NEXT_ID.store(1, Ordering::Relaxed);
41}
42
43/// Invoke a registered callback. Called from the FFI boundary.
44///
45/// Uses a take-execute-replace pattern to avoid holding the registry lock
46/// during callback execution, which would deadlock if the callback
47/// registers, unregisters, or invokes other delegates.
48pub fn invoke(callback_id: u64, params: *mut u8) {
49    // 1. Briefly lock, take the callback out (replace with None).
50    let mut cb = {
51        let mut reg = registry().lock().unwrap();
52        reg.get_mut(&callback_id).and_then(|slot| slot.take())
53    };
54
55    // 2. Execute outside the lock — callback may freely access the registry.
56    if let Some(ref mut f) = cb {
57        f(params);
58    }
59
60    // 3. Put back only if the slot still exists and is None.
61    //    If unregister_callback was called during execution, the entry was
62    //    removed entirely, so get_mut returns None and we drop the callback.
63    if let Some(f) = cb {
64        let mut reg = registry().lock().unwrap();
65        if let Some(slot) = reg.get_mut(&callback_id) {
66            if slot.is_none() {
67                *slot = Some(f);
68            }
69        }
70    }
71}
72
73// ---------------------------------------------------------------------------
74// DelegateBinding — RAII handle for delegate bindings
75// ---------------------------------------------------------------------------
76
77/// RAII handle that unbinds a delegate and unregisters the callback on drop.
78pub struct DelegateBinding {
79    callback_id: u64,
80    owner: UObjectHandle,
81    prop: FPropertyHandle,
82    is_multicast: bool,
83}
84
85impl DelegateBinding {
86    /// Create a new binding handle. Should only be called by generated code.
87    pub fn new(
88        callback_id: u64,
89        owner: UObjectHandle,
90        prop: FPropertyHandle,
91        is_multicast: bool,
92    ) -> Self {
93        Self {
94            callback_id,
95            owner,
96            prop,
97            is_multicast,
98        }
99    }
100
101    /// Get the callback ID.
102    pub fn callback_id(&self) -> u64 {
103        self.callback_id
104    }
105
106    /// Manually unbind without waiting for drop. Consumes self.
107    pub fn unbind(self) {
108        // Drop will handle the cleanup.
109    }
110}
111
112impl Drop for DelegateBinding {
113    fn drop(&mut self) {
114        // Unregister from Rust registry.
115        unregister_callback(self.callback_id);
116
117        // Unbind on the C++ side.
118        unsafe {
119            let api = crate::api::api();
120            if !api.delegate.is_null() {
121                if self.is_multicast {
122                    let _ = ((*api.delegate).remove_multicast)(
123                        self.owner,
124                        self.prop,
125                        self.callback_id,
126                    );
127                } else {
128                    let _ = ((*api.delegate).unbind_delegate)(self.owner, self.prop);
129                }
130            }
131        }
132    }
133}
134
135// ---------------------------------------------------------------------------
136// High-level bind helpers (used by generated code)
137// ---------------------------------------------------------------------------
138
139/// Bind a Rust closure to a unicast delegate property.
140pub fn bind_unicast(
141    owner: UObjectHandle,
142    prop: FPropertyHandle,
143    callback: impl FnMut(*mut u8) + Send + 'static,
144) -> UikaResult<DelegateBinding> {
145    let id = register_callback(callback);
146    let result = unsafe { ((*crate::api::api().delegate).bind_delegate)(owner, prop, id) };
147    if result != UikaErrorCode::Ok {
148        unregister_callback(id);
149        check_ffi(result)?;
150    }
151    Ok(DelegateBinding::new(id, owner, prop, false))
152}
153
154/// Add a Rust closure to a multicast delegate property.
155pub fn bind_multicast(
156    owner: UObjectHandle,
157    prop: FPropertyHandle,
158    callback: impl FnMut(*mut u8) + Send + 'static,
159) -> UikaResult<DelegateBinding> {
160    let id = register_callback(callback);
161    let result = unsafe { ((*crate::api::api().delegate).add_multicast)(owner, prop, id) };
162    if result != UikaErrorCode::Ok {
163        unregister_callback(id);
164        check_ffi(result)?;
165    }
166    Ok(DelegateBinding::new(id, owner, prop, true))
167}