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("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    if actual_type != expected_type {
220        panic!("Actor type mismatch for '{id}': expected {expected_type:?}, found {actual_type:?}");
221    }
222
223    ActorRef {
224        actor_rc,
225        _marker: PhantomData,
226    }
227}
228
229/// Attempts to get a guard providing mutable access to the registered actor.
230///
231/// Returns `None` if the actor is not found or the type doesn't match.
232///
233/// # Safety
234///
235/// See [`get_actor_unchecked`] for safety requirements. The same aliasing
236/// and thread-safety constraints apply.
237#[must_use]
238pub fn try_get_actor_unchecked<T: Actor>(id: &Ustr) -> Option<ActorRef<T>> {
239    let registry = get_actor_registry();
240    let actor_rc = registry.get(id)?;
241
242    // SAFETY: Get a reference to check the type before casting
243    let actor_ref = unsafe { &*actor_rc.get() };
244    let actual_type = actor_ref.as_any().type_id();
245    let expected_type = TypeId::of::<T>();
246
247    if actual_type != expected_type {
248        return None;
249    }
250
251    Some(ActorRef {
252        actor_rc,
253        _marker: PhantomData,
254    })
255}
256
257/// Checks if an actor with the `id` exists in the registry.
258pub fn actor_exists(id: &Ustr) -> bool {
259    get_actor_registry().contains(id)
260}
261
262/// Returns the number of registered actors.
263pub fn actor_count() -> usize {
264    get_actor_registry().len()
265}
266
267#[cfg(test)]
268/// Clears the actor registry (for test isolation).
269pub fn clear_actor_registry() {
270    let registry = get_actor_registry();
271    registry.actors.borrow_mut().clear();
272}
273
274#[cfg(test)]
275mod tests {
276    use std::any::Any;
277
278    use rstest::rstest;
279
280    use super::*;
281
282    #[derive(Debug)]
283    struct TestActor {
284        id: Ustr,
285        value: i32,
286    }
287
288    impl Actor for TestActor {
289        fn id(&self) -> Ustr {
290            self.id
291        }
292        fn handle(&mut self, _msg: &dyn Any) {}
293        fn as_any(&self) -> &dyn Any {
294            self
295        }
296    }
297
298    #[rstest]
299    fn test_register_and_get_actor() {
300        clear_actor_registry();
301
302        let id = Ustr::from("test-actor");
303        let actor = TestActor { id, value: 42 };
304        register_actor(actor);
305
306        let actor_ref = get_actor_unchecked::<TestActor>(&id);
307        assert_eq!(actor_ref.value, 42);
308    }
309
310    #[rstest]
311    fn test_mutation_through_reference() {
312        clear_actor_registry();
313
314        let id = Ustr::from("test-actor-mut");
315        let actor = TestActor { id, value: 0 };
316        register_actor(actor);
317
318        let mut actor_ref = get_actor_unchecked::<TestActor>(&id);
319        actor_ref.value = 999;
320
321        let actor_ref2 = get_actor_unchecked::<TestActor>(&id);
322        assert_eq!(actor_ref2.value, 999);
323    }
324
325    #[rstest]
326    fn test_try_get_returns_none_for_missing() {
327        clear_actor_registry();
328
329        let id = Ustr::from("nonexistent");
330        let result = try_get_actor_unchecked::<TestActor>(&id);
331        assert!(result.is_none());
332    }
333
334    #[rstest]
335    fn test_try_get_returns_none_for_wrong_type() {
336        clear_actor_registry();
337
338        #[derive(Debug)]
339        struct OtherActor {
340            id: Ustr,
341        }
342
343        impl Actor for OtherActor {
344            fn id(&self) -> Ustr {
345                self.id
346            }
347            fn handle(&mut self, _msg: &dyn Any) {}
348            fn as_any(&self) -> &dyn Any {
349                self
350            }
351        }
352
353        let id = Ustr::from("other-actor");
354        let actor = OtherActor { id };
355        register_actor(actor);
356
357        let result = try_get_actor_unchecked::<TestActor>(&id);
358        assert!(result.is_none());
359    }
360}