Skip to main content

zerodds_rtc/
object.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! `LightweightRTObject` + State-Machine — Spec §5.2.2.2.
5
6use alloc::vec::Vec;
7use core::sync::atomic::{AtomicU32, Ordering};
8
9use crate::lifecycle::{ComponentAction, LifeCycleState, is_valid_transition};
10use crate::return_code::ReturnCode;
11
12/// `ExecutionContextHandle_t` (Spec §5.2.2.8, S. 30) — opaque Handle
13/// fuer die Assoziation eines RTC mit einem Execution-Context.
14pub type ExecutionContextHandle = u32;
15
16/// Sentinel-Wert "kein Handle" (analog `INVALID_HANDLE_VALUE`).
17pub const INVALID_HANDLE: ExecutionContextHandle = 0;
18
19/// Monoton wachsende Handle-Generation.
20fn next_handle() -> ExecutionContextHandle {
21    static COUNTER: AtomicU32 = AtomicU32::new(1);
22    let n = COUNTER.fetch_add(1, Ordering::SeqCst);
23    if n == 0 {
24        COUNTER.fetch_add(1, Ordering::SeqCst)
25    } else {
26        n
27    }
28}
29
30/// `LightweightRTObject` — Spec §5.2.2.2 (S. 12-19).
31///
32/// Verwaltet:
33/// * Lifecycle-State pro Execution-Context (Spec §5.2.2.3).
34/// * Liste aller Contexts in denen das RTC participates.
35/// * Owner-Context-Handle (das RTC kann selbst Owner eines Contexts
36///   sein — autonomous RTC, Spec §5.2.2.5).
37/// * Reference auf den ComponentAction-Callbacks (`Box<dyn>` damit
38///   Caller eigene Behavior einbauen kann).
39///
40/// State-Machine wird zentral hier durchgesetzt — alle Operations
41/// pruefen Pre-Conditions und liefern `PRECONDITION_NOT_MET` im
42/// Fehlerfall (Spec §5.2.2.2.x).
43pub struct LightweightRtObject {
44    /// Globaler Lifecycle-State (Created → Alive → Finalized).
45    /// Spec §5.2.2.2.3 (`is_alive`): "is alive or not regardless of
46    /// the execution context from which it is observed".
47    is_alive: bool,
48    /// Pro-Context-State (Map handle → state).
49    contexts: Vec<ContextEntry>,
50    /// User-supplied callbacks.
51    callbacks: alloc::boxed::Box<dyn ComponentAction>,
52}
53
54/// Per-Context-Status-Eintrag.
55#[derive(Debug, Clone)]
56struct ContextEntry {
57    handle: ExecutionContextHandle,
58    state: LifeCycleState,
59}
60
61impl core::fmt::Debug for LightweightRtObject {
62    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63        f.debug_struct("LightweightRtObject")
64            .field("is_alive", &self.is_alive)
65            .field("contexts", &self.contexts)
66            .finish_non_exhaustive()
67    }
68}
69
70impl LightweightRtObject {
71    /// Konstruiert ein neues, noch nicht initialisiertes RTC im
72    /// `Created`-Zustand. Spec §5.2.2.3.1.
73    #[must_use]
74    pub fn new(callbacks: alloc::boxed::Box<dyn ComponentAction>) -> Self {
75        Self {
76            is_alive: false,
77            contexts: Vec::new(),
78            callbacks,
79        }
80    }
81
82    /// Spec §5.2.2.2.1 — `initialize`: Created → Alive (Inactive in
83    /// jedem attached Context).
84    ///
85    /// "An RTC may be initialized only while it is in the Created
86    /// state. Any attempt to invoke this operation while in another
87    /// state shall fail with PRECONDITION_NOT_MET."
88    pub fn initialize(&mut self) -> ReturnCode {
89        if self.is_alive {
90            return ReturnCode::PreconditionNotMet;
91        }
92        let cb = self.callbacks.on_initialize();
93        if !cb.is_ok() {
94            return cb;
95        }
96        self.is_alive = true;
97        ReturnCode::Ok
98    }
99
100    /// Spec §5.2.2.2.2 — `finalize`: Alive → Created (no longer
101    /// attached to any context).
102    ///
103    /// "An RTC may not be finalized while it is participating in any
104    /// execution context."
105    pub fn finalize(&mut self) -> ReturnCode {
106        if !self.is_alive {
107            // Created → finalize: PRECONDITION_NOT_MET.
108            return ReturnCode::PreconditionNotMet;
109        }
110        if !self.contexts.is_empty() {
111            return ReturnCode::PreconditionNotMet;
112        }
113        let cb = self.callbacks.on_finalize();
114        if !cb.is_ok() {
115            return cb;
116        }
117        self.is_alive = false;
118        ReturnCode::Ok
119    }
120
121    /// Spec §5.2.2.2.3 — `is_alive`. "is alive or not regardless of
122    /// the execution context from which it is observed".
123    #[must_use]
124    pub const fn is_alive(&self) -> bool {
125        self.is_alive
126    }
127
128    /// Spec §5.2.2.2.5 — `attach_context`: registriert das RTC fuer
129    /// einen Context. Liefert ein neues Handle.
130    ///
131    /// "This operation is intended to be invoked by
132    /// ExecutionContextOperations::add_component. It is not intended
133    /// for use by other clients."
134    pub fn attach_context(&mut self) -> Result<ExecutionContextHandle, ReturnCode> {
135        if !self.is_alive {
136            return Err(ReturnCode::PreconditionNotMet);
137        }
138        let handle = next_handle();
139        self.contexts.push(ContextEntry {
140            handle,
141            state: LifeCycleState::Inactive,
142        });
143        Ok(handle)
144    }
145
146    /// Spec §5.2.2.2.6 — `detach_context`. "may not be invoked if
147    /// this RTC is Active in the indicated execution context".
148    pub fn detach_context(&mut self, handle: ExecutionContextHandle) -> ReturnCode {
149        let Some(idx) = self.contexts.iter().position(|c| c.handle == handle) else {
150            return ReturnCode::PreconditionNotMet;
151        };
152        if self.contexts[idx].state == LifeCycleState::Active {
153            return ReturnCode::PreconditionNotMet;
154        }
155        self.contexts.swap_remove(idx);
156        ReturnCode::Ok
157    }
158
159    /// Spec §5.2.2.2.9 — `get_participating_contexts`. Liefert eine
160    /// Liste der Handles in denen dieses RTC participates.
161    #[must_use]
162    pub fn get_participating_contexts(&self) -> Vec<ExecutionContextHandle> {
163        self.contexts.iter().map(|c| c.handle).collect()
164    }
165
166    /// Spec §5.2.2.6.x via Caller invoked — Ueberprueft ob das RTC im
167    /// gegebenen Context im erwarteten State ist.
168    #[must_use]
169    pub fn get_context_state(&self, handle: ExecutionContextHandle) -> Option<LifeCycleState> {
170        self.contexts
171            .iter()
172            .find(|c| c.handle == handle)
173            .map(|c| c.state)
174    }
175
176    /// Internal: Inactive → Active im gegebenen Context. Wird von
177    /// `ExecutionContext::activate_component` invoked. Spec §5.2.2.6.8.
178    pub(crate) fn activate(&mut self, handle: ExecutionContextHandle) -> ReturnCode {
179        let Some(entry) = self.contexts.iter_mut().find(|c| c.handle == handle) else {
180            return ReturnCode::BadParameter;
181        };
182        if !is_valid_transition(entry.state, LifeCycleState::Active) {
183            return ReturnCode::PreconditionNotMet;
184        }
185        entry.state = LifeCycleState::Active;
186        let cb = self.callbacks.on_activated(handle);
187        if !cb.is_ok() {
188            // Spec §5.2.2.4.7: on_activated-Failure → Active → Error.
189            entry.state = LifeCycleState::Error;
190            self.callbacks.on_aborting(handle);
191            return cb;
192        }
193        ReturnCode::Ok
194    }
195
196    /// Internal: Active → Inactive. Spec §5.2.2.6.9.
197    pub(crate) fn deactivate(&mut self, handle: ExecutionContextHandle) -> ReturnCode {
198        let Some(entry) = self.contexts.iter_mut().find(|c| c.handle == handle) else {
199            return ReturnCode::BadParameter;
200        };
201        if !is_valid_transition(entry.state, LifeCycleState::Inactive) {
202            return ReturnCode::PreconditionNotMet;
203        }
204        entry.state = LifeCycleState::Inactive;
205        self.callbacks.on_deactivated(handle)
206    }
207
208    /// Internal: Error → Inactive via `reset_component`. Spec
209    /// §5.2.2.6.10.
210    pub(crate) fn reset(&mut self, handle: ExecutionContextHandle) -> ReturnCode {
211        let Some(entry) = self.contexts.iter_mut().find(|c| c.handle == handle) else {
212            return ReturnCode::BadParameter;
213        };
214        if entry.state != LifeCycleState::Error {
215            return ReturnCode::PreconditionNotMet;
216        }
217        let cb = self.callbacks.on_reset(handle);
218        if cb.is_ok() {
219            entry.state = LifeCycleState::Inactive;
220        }
221        cb
222    }
223
224    /// Forciert Active → Error nach Callback-Fehler in User-Code.
225    /// Spec §5.2.2.4.7 — `on_aborting` wird einmalig invoked,
226    /// danach uebernimmt `on_error` (siehe Periodic-Tick-Loop).
227    pub fn transition_to_error(&mut self, handle: ExecutionContextHandle) {
228        if let Some(entry) = self.contexts.iter_mut().find(|c| c.handle == handle) {
229            if entry.state == LifeCycleState::Active {
230                entry.state = LifeCycleState::Error;
231                self.callbacks.on_aborting(handle);
232            }
233        }
234    }
235
236    /// Liefert mutable-Zugriff auf die Callbacks. Wird vom
237    /// `ExecutionContext::tick`-Loop benoetigt, um die periodischen
238    /// `on_execute`/`on_state_update`/`on_error`-Callbacks zu invoken.
239    pub fn callbacks_mut(&mut self) -> &mut dyn ComponentAction {
240        self.callbacks.as_mut()
241    }
242}
243
244#[cfg(test)]
245#[allow(clippy::expect_used)]
246mod tests {
247    use super::*;
248
249    struct CountingCallbacks {
250        initialize: u32,
251        finalize: u32,
252        activated: u32,
253        deactivated: u32,
254        reset: u32,
255        force_init_fail: bool,
256    }
257
258    impl ComponentAction for CountingCallbacks {
259        fn on_initialize(&mut self) -> ReturnCode {
260            self.initialize += 1;
261            if self.force_init_fail {
262                ReturnCode::Error
263            } else {
264                ReturnCode::Ok
265            }
266        }
267        fn on_finalize(&mut self) -> ReturnCode {
268            self.finalize += 1;
269            ReturnCode::Ok
270        }
271        fn on_activated(&mut self, _h: u32) -> ReturnCode {
272            self.activated += 1;
273            ReturnCode::Ok
274        }
275        fn on_deactivated(&mut self, _h: u32) -> ReturnCode {
276            self.deactivated += 1;
277            ReturnCode::Ok
278        }
279        fn on_reset(&mut self, _h: u32) -> ReturnCode {
280            self.reset += 1;
281            ReturnCode::Ok
282        }
283    }
284
285    fn make() -> LightweightRtObject {
286        LightweightRtObject::new(alloc::boxed::Box::new(CountingCallbacks {
287            initialize: 0,
288            finalize: 0,
289            activated: 0,
290            deactivated: 0,
291            reset: 0,
292            force_init_fail: false,
293        }))
294    }
295
296    #[test]
297    fn fresh_rtc_is_not_alive() {
298        // Spec §5.2.2.3.1.
299        let r = make();
300        assert!(!r.is_alive());
301    }
302
303    #[test]
304    fn initialize_then_finalize_round_trips_alive_flag() {
305        // Spec §5.2.2.2.1 + §5.2.2.2.2.
306        let mut r = make();
307        assert_eq!(r.initialize(), ReturnCode::Ok);
308        assert!(r.is_alive());
309        assert_eq!(r.finalize(), ReturnCode::Ok);
310        assert!(!r.is_alive());
311    }
312
313    #[test]
314    fn double_initialize_yields_precondition_not_met() {
315        // Spec §5.2.2.2.1 — initialize only valid in Created state.
316        let mut r = make();
317        assert_eq!(r.initialize(), ReturnCode::Ok);
318        assert_eq!(r.initialize(), ReturnCode::PreconditionNotMet);
319    }
320
321    #[test]
322    fn finalize_in_created_state_yields_precondition_not_met() {
323        // Spec §5.2.2.2.2.
324        let mut r = make();
325        assert_eq!(r.finalize(), ReturnCode::PreconditionNotMet);
326    }
327
328    #[test]
329    fn finalize_with_attached_context_yields_precondition_not_met() {
330        // Spec §5.2.2.2.2 — must remove with detach first.
331        let mut r = make();
332        r.initialize();
333        let _ = r.attach_context().expect("attach ok");
334        assert_eq!(r.finalize(), ReturnCode::PreconditionNotMet);
335    }
336
337    #[test]
338    fn attach_context_in_created_state_fails() {
339        // Spec §5.2.2.2.5 + §5.2.2.5 — implicit pre-condition is_alive.
340        let mut r = make();
341        assert!(matches!(
342            r.attach_context(),
343            Err(ReturnCode::PreconditionNotMet)
344        ));
345    }
346
347    #[test]
348    fn attach_then_detach_works() {
349        let mut r = make();
350        r.initialize();
351        let h = r.attach_context().expect("attach");
352        assert_eq!(r.get_participating_contexts(), alloc::vec![h]);
353        assert_eq!(r.detach_context(h), ReturnCode::Ok);
354        assert!(r.get_participating_contexts().is_empty());
355    }
356
357    #[test]
358    fn detach_unknown_handle_yields_precondition_not_met() {
359        // Spec §5.2.2.2.6.
360        let mut r = make();
361        r.initialize();
362        assert_eq!(r.detach_context(99_999), ReturnCode::PreconditionNotMet);
363    }
364
365    #[test]
366    fn detach_active_rtc_yields_precondition_not_met() {
367        // Spec §5.2.2.2.6: "may not be invoked if this RTC is Active".
368        let mut r = make();
369        r.initialize();
370        let h = r.attach_context().expect("attach");
371        assert_eq!(r.activate(h), ReturnCode::Ok);
372        assert_eq!(r.detach_context(h), ReturnCode::PreconditionNotMet);
373    }
374
375    #[test]
376    fn activate_inactive_rtc_invokes_on_activated() {
377        let mut r = make();
378        r.initialize();
379        let h = r.attach_context().expect("attach");
380        assert_eq!(r.activate(h), ReturnCode::Ok);
381        assert_eq!(r.get_context_state(h), Some(LifeCycleState::Active));
382    }
383
384    #[test]
385    fn deactivate_active_rtc_invokes_on_deactivated() {
386        let mut r = make();
387        r.initialize();
388        let h = r.attach_context().expect("attach");
389        r.activate(h);
390        assert_eq!(r.deactivate(h), ReturnCode::Ok);
391        assert_eq!(r.get_context_state(h), Some(LifeCycleState::Inactive));
392    }
393
394    #[test]
395    fn reset_only_works_from_error_state() {
396        // Spec §5.2.2.6.10.
397        let mut r = make();
398        r.initialize();
399        let h = r.attach_context().expect("attach");
400        // Inactive → reset = PRECONDITION_NOT_MET.
401        assert_eq!(r.reset(h), ReturnCode::PreconditionNotMet);
402        // Activate then force into Error.
403        r.activate(h);
404        r.transition_to_error(h);
405        assert_eq!(r.get_context_state(h), Some(LifeCycleState::Error));
406        // Now reset works.
407        assert_eq!(r.reset(h), ReturnCode::Ok);
408        assert_eq!(r.get_context_state(h), Some(LifeCycleState::Inactive));
409    }
410
411    #[test]
412    fn initialize_failure_keeps_rtc_in_created_state() {
413        // Spec §5.2.2.2.1 — wenn on_initialize fehlschlaegt, bleibt
414        // RTC im Created-State.
415        let mut r = LightweightRtObject::new(alloc::boxed::Box::new(CountingCallbacks {
416            initialize: 0,
417            finalize: 0,
418            activated: 0,
419            deactivated: 0,
420            reset: 0,
421            force_init_fail: true,
422        }));
423        assert_eq!(r.initialize(), ReturnCode::Error);
424        assert!(!r.is_alive());
425    }
426
427    #[test]
428    fn handles_are_unique_across_attaches() {
429        let mut r = make();
430        r.initialize();
431        let h1 = r.attach_context().expect("attach1");
432        let h2 = r.attach_context().expect("attach2");
433        assert_ne!(h1, h2);
434        assert_ne!(h1, INVALID_HANDLE);
435        assert_ne!(h2, INVALID_HANDLE);
436    }
437}