Skip to main content

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