nautilus_common/actor/
registry.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Thread-local actor registry with lifetime-safe access guards.
17//!
18//! # Design
19//!
20//! The actor registry stores actors in thread-local storage and provides access via
21//! [`ActorRef<T>`] guards. This design addresses several constraints:
22//!
23//! - **Use-after-free prevention**: `ActorRef` holds an `Rc` clone, keeping the actor
24//!   alive even if removed from the registry while the guard exists.
25//! - **Re-entrant callbacks**: Message handlers frequently call back into the registry
26//!   to access other actors. Unlike `RefCell`-style borrow tracking, multiple `ActorRef`
27//!   guards can exist simultaneously without panicking.
28//! - **No `'static` lifetime lie**: Previous designs returned `&'static mut T`, which
29//!   didn't reflect actual validity. The guard-based approach ties the borrow to the
30//!   guard's lifetime.
31//!
32//! # Limitations
33//!
34//! - **Aliasing not prevented**: Two guards can exist for the same actor simultaneously,
35//!   allowing aliased mutable access. This is technically undefined behavior but is
36//!   required by the re-entrant callback pattern. Higher-level discipline is required.
37//! - **Thread-local only**: Guards must not be sent across threads.
38
39use std::{
40    any::TypeId,
41    cell::{RefCell, UnsafeCell},
42    fmt::Debug,
43    marker::PhantomData,
44    ops::{Deref, DerefMut},
45    rc::Rc,
46};
47
48use ahash::AHashMap;
49use ustr::Ustr;
50
51use super::Actor;
52
53/// A guard providing mutable access to an actor.
54///
55/// This guard holds an `Rc` reference to keep the actor alive, preventing
56/// use-after-free if the actor is removed from the registry while the guard
57/// exists. The guard implements `Deref` and `DerefMut` for ergonomic access.
58///
59/// # Safety
60///
61/// While this guard prevents use-after-free from registry removal, it does not
62/// prevent aliasing. Multiple `ActorRef` instances can exist for the same actor
63/// simultaneously, which is technically undefined behavior but is required by
64/// the re-entrant callback pattern in this codebase.
65pub struct ActorRef<T: Actor> {
66    actor_rc: Rc<UnsafeCell<dyn Actor>>,
67    _marker: PhantomData<T>,
68}
69
70impl<T: Actor> Debug for ActorRef<T> {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct(stringify!(ActorRef))
73            .field("actor_id", &self.deref().id())
74            .finish()
75    }
76}
77
78impl<T: Actor> Deref for ActorRef<T> {
79    type Target = T;
80
81    fn deref(&self) -> &Self::Target {
82        // SAFETY: Type was verified at construction time
83        unsafe { &*(self.actor_rc.get() as *const T) }
84    }
85}
86
87impl<T: Actor> DerefMut for ActorRef<T> {
88    fn deref_mut(&mut self) -> &mut Self::Target {
89        // SAFETY: Type was verified at construction time
90        unsafe { &mut *(self.actor_rc.get() as *mut T) }
91    }
92}
93
94thread_local! {
95    static ACTOR_REGISTRY: ActorRegistry = ActorRegistry::new();
96}
97
98/// Registry for storing actors.
99pub struct ActorRegistry {
100    actors: RefCell<AHashMap<Ustr, Rc<UnsafeCell<dyn Actor>>>>,
101}
102
103impl Debug for ActorRegistry {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        let actors_ref = self.actors.borrow();
106        let keys: Vec<&Ustr> = actors_ref.keys().collect();
107        f.debug_struct(stringify!(ActorRegistry))
108            .field("actors", &keys)
109            .finish()
110    }
111}
112
113impl Default for ActorRegistry {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119impl ActorRegistry {
120    pub fn new() -> Self {
121        Self {
122            actors: RefCell::new(AHashMap::new()),
123        }
124    }
125
126    pub fn insert(&self, id: Ustr, actor: Rc<UnsafeCell<dyn Actor>>) {
127        let mut actors = self.actors.borrow_mut();
128        if actors.contains_key(&id) {
129            log::warn!("Replacing existing actor with id: {id}");
130        }
131        actors.insert(id, actor);
132    }
133
134    pub fn get(&self, id: &Ustr) -> Option<Rc<UnsafeCell<dyn Actor>>> {
135        self.actors.borrow().get(id).cloned()
136    }
137
138    /// Returns the number of registered actors.
139    pub fn len(&self) -> usize {
140        self.actors.borrow().len()
141    }
142
143    /// Checks if the registry is empty.
144    pub fn is_empty(&self) -> bool {
145        self.actors.borrow().is_empty()
146    }
147
148    /// Removes an actor from the registry.
149    pub fn remove(&self, id: &Ustr) -> Option<Rc<UnsafeCell<dyn Actor>>> {
150        self.actors.borrow_mut().remove(id)
151    }
152
153    /// Checks if an actor with the `id` exists.
154    pub fn contains(&self, id: &Ustr) -> bool {
155        self.actors.borrow().contains_key(id)
156    }
157}
158
159pub fn get_actor_registry() -> &'static ActorRegistry {
160    ACTOR_REGISTRY.with(|registry| unsafe {
161        // SAFETY: We return a static reference that lives for the lifetime of the thread.
162        // Since this is thread_local storage, each thread has its own instance.
163        // The transmute extends the lifetime to 'static which is safe because
164        // thread_local ensures the registry lives for the thread's entire lifetime.
165        std::mem::transmute::<&ActorRegistry, &'static ActorRegistry>(registry)
166    })
167}
168
169/// Registers an actor.
170pub fn register_actor<T>(actor: T) -> Rc<UnsafeCell<T>>
171where
172    T: Actor + 'static,
173{
174    let actor_id = actor.id();
175    let actor_ref = Rc::new(UnsafeCell::new(actor));
176
177    // Register as Actor (message handling only)
178    let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = actor_ref.clone();
179    get_actor_registry().insert(actor_id, actor_trait_ref);
180
181    actor_ref
182}
183
184pub fn get_actor(id: &Ustr) -> Option<Rc<UnsafeCell<dyn Actor>>> {
185    get_actor_registry().get(id)
186}
187
188/// Returns a guard providing mutable access to the registered actor of type `T`.
189///
190/// The returned [`ActorRef`] holds an `Rc` to keep the actor alive, preventing
191/// use-after-free if the actor is removed from the registry.
192///
193/// # Panics
194///
195/// - Panics if no actor with the specified `id` is found in the registry.
196/// - Panics if the stored actor is not of type `T`.
197///
198/// # Safety
199///
200/// While this function is not marked `unsafe`, aliasing constraints apply:
201///
202/// - **Aliasing**: The caller should ensure no other mutable references to the same
203///   actor exist simultaneously. The callback-based message handling pattern in this
204///   codebase requires re-entrant access, which technically violates this invariant.
205/// - **Thread safety**: The registry is thread-local; do not send guards across
206///   threads.
207#[must_use]
208pub fn get_actor_unchecked<T: Actor>(id: &Ustr) -> ActorRef<T> {
209    let registry = get_actor_registry();
210    let actor_rc = registry
211        .get(id)
212        .unwrap_or_else(|| panic!("Actor for {id} not found"));
213
214    // SAFETY: Get a reference to check the type before casting
215    let actor_ref = unsafe { &*actor_rc.get() };
216    let actual_type = actor_ref.as_any().type_id();
217    let expected_type = TypeId::of::<T>();
218
219    assert!(
220        actual_type == expected_type,
221        "Actor type mismatch for '{id}': expected {expected_type:?}, found {actual_type:?}"
222    );
223
224    ActorRef {
225        actor_rc,
226        _marker: PhantomData,
227    }
228}
229
230/// Attempts to get a guard providing mutable access to the registered actor.
231///
232/// Returns `None` if the actor is not found or the type doesn't match.
233///
234/// # Safety
235///
236/// See [`get_actor_unchecked`] for safety requirements. The same aliasing
237/// and thread-safety constraints apply.
238#[must_use]
239pub fn try_get_actor_unchecked<T: Actor>(id: &Ustr) -> Option<ActorRef<T>> {
240    let registry = get_actor_registry();
241    let actor_rc = registry.get(id)?;
242
243    // SAFETY: Get a reference to check the type before casting
244    let actor_ref = unsafe { &*actor_rc.get() };
245    let actual_type = actor_ref.as_any().type_id();
246    let expected_type = TypeId::of::<T>();
247
248    if actual_type != expected_type {
249        return None;
250    }
251
252    Some(ActorRef {
253        actor_rc,
254        _marker: PhantomData,
255    })
256}
257
258/// Checks if an actor with the `id` exists in the registry.
259pub fn actor_exists(id: &Ustr) -> bool {
260    get_actor_registry().contains(id)
261}
262
263/// Returns the number of registered actors.
264pub fn actor_count() -> usize {
265    get_actor_registry().len()
266}
267
268#[cfg(test)]
269/// Clears the actor registry (for test isolation).
270pub fn clear_actor_registry() {
271    let registry = get_actor_registry();
272    registry.actors.borrow_mut().clear();
273}
274
275#[cfg(test)]
276mod tests {
277    use std::any::Any;
278
279    use rstest::rstest;
280
281    use super::*;
282
283    #[derive(Debug)]
284    struct TestActor {
285        id: Ustr,
286        value: i32,
287    }
288
289    impl Actor for TestActor {
290        fn id(&self) -> Ustr {
291            self.id
292        }
293        fn handle(&mut self, _msg: &dyn Any) {}
294        fn as_any(&self) -> &dyn Any {
295            self
296        }
297    }
298
299    #[rstest]
300    fn test_register_and_get_actor() {
301        clear_actor_registry();
302
303        let id = Ustr::from("test-actor");
304        let actor = TestActor { id, value: 42 };
305        register_actor(actor);
306
307        let actor_ref = get_actor_unchecked::<TestActor>(&id);
308        assert_eq!(actor_ref.value, 42);
309    }
310
311    #[rstest]
312    fn test_mutation_through_reference() {
313        clear_actor_registry();
314
315        let id = Ustr::from("test-actor-mut");
316        let actor = TestActor { id, value: 0 };
317        register_actor(actor);
318
319        let mut actor_ref = get_actor_unchecked::<TestActor>(&id);
320        actor_ref.value = 999;
321
322        let actor_ref2 = get_actor_unchecked::<TestActor>(&id);
323        assert_eq!(actor_ref2.value, 999);
324    }
325
326    #[rstest]
327    fn test_try_get_returns_none_for_missing() {
328        clear_actor_registry();
329
330        let id = Ustr::from("nonexistent");
331        let result = try_get_actor_unchecked::<TestActor>(&id);
332        assert!(result.is_none());
333    }
334
335    #[rstest]
336    fn test_try_get_returns_none_for_wrong_type() {
337        #[derive(Debug)]
338        struct OtherActor {
339            id: Ustr,
340        }
341
342        impl Actor for OtherActor {
343            fn id(&self) -> Ustr {
344                self.id
345            }
346            fn handle(&mut self, _msg: &dyn Any) {}
347            fn as_any(&self) -> &dyn Any {
348                self
349            }
350        }
351
352        clear_actor_registry();
353
354        let id = Ustr::from("other-actor");
355        let actor = OtherActor { id };
356        register_actor(actor);
357
358        let result = try_get_actor_unchecked::<TestActor>(&id);
359        assert!(result.is_none());
360    }
361}