Skip to main content

zerodds_dcps/
entity.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Entity lifecycle (DDS DCPS 1.4 §2.2.2.1) — common base for
4//! `DomainParticipant`, `Publisher`, `Subscriber`, `Topic`,
5//! `DataWriter`, `DataReader`.
6//!
7//! Spec behavior (§2.2.2.1.1 entity base):
8//! 1. **Lifecycle:** `create_*` → `enable()` → operational → `delete_*`.
9//!    Before `enable()` the entity is inert (no discovery, no wire
10//!    activity); set_qos on all fields is allowed.
11//! 2. **set_qos** after `enable()`: only fields with "Changeable=YES"
12//!    may be changed — otherwise [`DdsError::ImmutablePolicy`]
13//!    (§2.2.3 Tab. 2.13 column "Changeable").
14//! 3. **enable()** is idempotent. If the parent entity (participant)
15//!    has `entity_factory.autoenable_created_entities=TRUE`, children
16//!    are automatically enabled on creation.
17//! 4. **StatusCondition** is the hook for the `WaitSet` —
18//!    `trigger_value()` returns true when a status whose bit is in the
19//!    `enabled_statuses` mask is active.
20//! 5. **InstanceHandle** is unique per entity (a local 64-bit counter,
21//!    not on the wire — see [`crate::instance_handle`]).
22//!
23//! This module provides the low-level [`Entity`] trait + [`EntityState`]
24//! as a building block. The implementations (Publisher, DataWriter,
25//! ...) hold an `Arc<EntityState>` and delegate the trait methods.
26
27extern crate alloc;
28
29use alloc::sync::Arc;
30use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
31
32use crate::error::{DdsError, Result};
33use crate::instance_handle::{InstanceHandle, InstanceHandleAllocator};
34
35/// Global allocator for entity InstanceHandles. One instance per
36/// process — handles are unique within the process.
37static ENTITY_HANDLE_ALLOCATOR: InstanceHandleAllocator = InstanceHandleAllocator::new();
38
39/// `StatusMask` — 32-bit bitmask of the status kinds (DCPS §2.2.4.1).
40/// Values from [`crate::psm_constants::status`].
41pub type StatusMask = u32;
42
43/// Atomic container for the entity lifecycle.
44#[derive(Debug)]
45pub struct EntityState {
46    enabled: AtomicBool,
47    /// `true` after a successful `delete_*()` — Spec §2.2.1.1.5
48    /// (RC ALREADY_DELETED). Public ops MUST call
49    /// [`Self::check_not_deleted`] before any effect.
50    deleted: AtomicBool,
51    instance_handle: InstanceHandle,
52    /// Bitmask of the status bits changed **since the last
53    /// `get_status_changes()` read**.
54    status_changes: AtomicU32,
55    /// Bitmask of the status bits covered by the listener (for the
56    /// bubble-up logic).
57    listener_mask: AtomicU32,
58}
59
60impl EntityState {
61    /// New state, initially **disabled** (spec default for all
62    /// entities except DomainParticipantFactory).
63    #[must_use]
64    pub fn new() -> Arc<Self> {
65        Arc::new(Self {
66            enabled: AtomicBool::new(false),
67            deleted: AtomicBool::new(false),
68            instance_handle: ENTITY_HANDLE_ALLOCATOR.allocate(),
69            status_changes: AtomicU32::new(0),
70            listener_mask: AtomicU32::new(0),
71        })
72    }
73
74    /// New state, **already enabled** — for DomainParticipantFactory
75    /// (Spec §2.2.2.1.4: the factory is always enabled).
76    #[must_use]
77    pub fn new_enabled() -> Arc<Self> {
78        Arc::new(Self {
79            enabled: AtomicBool::new(true),
80            deleted: AtomicBool::new(false),
81            instance_handle: ENTITY_HANDLE_ALLOCATOR.allocate(),
82            status_changes: AtomicU32::new(0),
83            listener_mask: AtomicU32::new(0),
84        })
85    }
86
87    /// True if the entity is enabled.
88    #[must_use]
89    pub fn is_enabled(&self) -> bool {
90        self.enabled.load(Ordering::Acquire)
91    }
92
93    /// Sets enabled=true (idempotent). Returns `true` if the call
94    /// performed the false→true transition (for cascade logic).
95    pub fn enable(&self) -> bool {
96        !self.enabled.swap(true, Ordering::AcqRel)
97    }
98
99    /// Local 64-bit identifier of this entity.
100    #[must_use]
101    pub fn instance_handle(&self) -> InstanceHandle {
102        self.instance_handle
103    }
104
105    /// Current status-changes mask. Reading does NOT clear — the
106    /// caller takes the relevant bits back itself via
107    /// [`Self::clear_status_changes`].
108    #[must_use]
109    pub fn status_changes(&self) -> StatusMask {
110        self.status_changes.load(Ordering::Acquire)
111    }
112
113    /// Sets additional status bits (called by the discovery/runtime
114    /// layer when a status event arrives).
115    pub fn set_status_bits(&self, bits: StatusMask) {
116        self.status_changes.fetch_or(bits, Ordering::AcqRel);
117    }
118
119    /// Clears the given bits from the status-changes mask (after the
120    /// caller's read).
121    pub fn clear_status_changes(&self, bits: StatusMask) {
122        self.status_changes.fetch_and(!bits, Ordering::AcqRel);
123    }
124
125    /// Set the listener mask — affects bubble-up.
126    pub fn set_listener_mask(&self, mask: StatusMask) {
127        self.listener_mask.store(mask, Ordering::Release);
128    }
129
130    /// Read the listener mask.
131    #[must_use]
132    pub fn listener_mask(&self) -> StatusMask {
133        self.listener_mask.load(Ordering::Acquire)
134    }
135
136    /// `true` if the entity has already gone through `delete_*`.
137    #[must_use]
138    pub fn is_deleted(&self) -> bool {
139        self.deleted.load(Ordering::Acquire)
140    }
141
142    /// Marks the entity as deleted (idempotent). Returns `true` on the
143    /// first call (false→true transition), `false` on subsequent
144    /// calls.
145    pub fn mark_deleted(&self) -> bool {
146        !self.deleted.swap(true, Ordering::AcqRel)
147    }
148
149    /// Guard helper for public ops: returns `Err(AlreadyDeleted)` if
150    /// the entity has already been deleted, otherwise `Ok(())`.
151    /// Usage pattern:
152    /// ```ignore
153    /// pub fn write(&self, sample: T) -> Result<()> {
154    ///     self.entity_state().check_not_deleted()?;
155    ///     // ... the actual logic ...
156    /// }
157    /// ```
158    ///
159    /// # Errors
160    /// `DdsError::AlreadyDeleted` if `is_deleted() == true`.
161    pub fn check_not_deleted(&self) -> crate::error::Result<()> {
162        if self.is_deleted() {
163            Err(crate::error::DdsError::AlreadyDeleted)
164        } else {
165            Ok(())
166        }
167    }
168
169    /// Guard helper: returns `Err(NotEnabled)` if the entity is not
170    /// enabled (Spec §2.2.2.1.1.7 RC NOT_ENABLED).
171    ///
172    /// # Errors
173    /// `DdsError::NotEnabled` if `is_enabled() == false`.
174    pub fn check_enabled(&self) -> crate::error::Result<()> {
175        if !self.is_enabled() {
176            Err(crate::error::DdsError::NotEnabled)
177        } else {
178            Ok(())
179        }
180    }
181}
182
183/// `StatusCondition` — Spec §2.2.2.1.6, the primary WaitSet hook.
184///
185/// Minimal form: carries an `enabled_statuses` mask + delegates
186/// `trigger_value()` to [`EntityState::status_changes`]. The object is
187/// fully integrated (set_enabled_statuses, attach to WaitSet).
188#[derive(Debug, Clone)]
189pub struct StatusCondition {
190    state: Arc<EntityState>,
191    enabled_statuses: Arc<AtomicU32>,
192}
193
194impl StatusCondition {
195    /// Constructor (internal; created by the entity).
196    #[must_use]
197    pub fn new(state: Arc<EntityState>) -> Self {
198        Self {
199            state,
200            enabled_statuses: Arc::new(AtomicU32::new(crate::psm_constants::status::ANY)),
201        }
202    }
203
204    /// Sets the `enabled_statuses` mask. Spec §2.2.2.1.6.
205    pub fn set_enabled_statuses(&self, mask: StatusMask) {
206        self.enabled_statuses.store(mask, Ordering::Release);
207    }
208
209    /// Returns the current `enabled_statuses` mask.
210    #[must_use]
211    pub fn enabled_statuses(&self) -> StatusMask {
212        self.enabled_statuses.load(Ordering::Acquire)
213    }
214
215    /// True if (status_changes & enabled_statuses) != 0.
216    /// Spec §2.2.2.1.6 trigger_value.
217    #[must_use]
218    pub fn trigger_value(&self) -> bool {
219        let enabled = self.enabled_statuses.load(Ordering::Acquire);
220        let changes = self.state.status_changes();
221        (enabled & changes) != 0
222    }
223
224    /// Returns the `InstanceHandle` of the entity to which this
225    /// StatusCondition is bound. Spec DCPS 1.4 §2.2.2.1.9
226    /// `get_entity()` — the Rust API returns the handle instead of a
227    /// `&dyn Entity` pointer, because the same `Arc<EntityState>` can
228    /// be held by multiple entity wrappers (DataReader/DataWriter/...);
229    /// the handle is the only identity that is stable beyond the
230    /// wrapper granularity.
231    #[must_use]
232    pub fn get_entity_handle(&self) -> InstanceHandle {
233        self.state.instance_handle()
234    }
235
236    /// Returns a shared reference to the underlying `EntityState`
237    /// (Spec §2.2.2.1.9 — direct path). Lets caller code inspect the
238    /// entity's status mask and lifecycle flags without going through
239    /// the entity wrapper.
240    #[must_use]
241    pub fn entity_state(&self) -> &Arc<EntityState> {
242        &self.state
243    }
244}
245
246/// Entity trait — common lifecycle API of the 6 entity types
247/// (DCPS §2.2.2.1).
248///
249/// Non-blocking, Send+Sync — all methods delegate to
250/// `Arc<EntityState>`.
251pub trait Entity {
252    /// QoS type for this entity (e.g. `DomainParticipantQos`,
253    /// `DataWriterQos`, ...).
254    type Qos: Clone;
255
256    /// Returns the current QoS (clone).
257    /// Spec §2.2.2.1.2 `get_qos`.
258    fn get_qos(&self) -> Self::Qos;
259
260    /// Changes the QoS. Before enable: everything allowed. After
261    /// enable: only fields with "Changeable=YES" — otherwise an
262    /// `ImmutablePolicy` error. Spec §2.2.2.1.2 `set_qos`.
263    ///
264    /// # Errors
265    /// * [`DdsError::ImmutablePolicy`] if an immutable field is to be
266    ///   changed after `enable()`.
267    /// * [`DdsError::InconsistentPolicy`] if the new QoS combination is
268    ///   inconsistent.
269    fn set_qos(&self, qos: Self::Qos) -> Result<()>;
270
271    /// Enables the entity (idempotent). Spec §2.2.2.1.4 `enable`.
272    ///
273    /// # Errors
274    /// [`DdsError::PreconditionNotMet`] if the parent entity is not
275    /// enabled (per spec, children cannot be enabled before the parent
276    /// — except the factory itself).
277    fn enable(&self) -> Result<()>;
278
279    /// True if the entity is already enabled.
280    fn is_enabled(&self) -> bool {
281        self.entity_state().is_enabled()
282    }
283
284    /// `StatusCondition` of this entity.
285    /// Spec §2.2.2.1.6 `get_status_condition`.
286    fn get_status_condition(&self) -> StatusCondition {
287        StatusCondition::new(self.entity_state())
288    }
289
290    /// Bitmask of the status kinds changed since the last read.
291    /// Spec §2.2.2.1.5 `get_status_changes`.
292    fn get_status_changes(&self) -> StatusMask {
293        self.entity_state().status_changes()
294    }
295
296    /// Local 64-bit identifier. Spec §2.2.2.1.7 `get_instance_handle`.
297    fn get_instance_handle(&self) -> InstanceHandle {
298        self.entity_state().instance_handle()
299    }
300
301    /// Internal accessor — each impl returns its `Arc<EntityState>`.
302    fn entity_state(&self) -> Arc<EntityState>;
303}
304
305/// Helper function: validates that a QoS field `policy_name` was not
306/// changed after enable. Used in `set_qos` impls:
307///
308/// ```ignore
309/// if state.is_enabled() && new.durability != old.durability {
310///     return Err(immutable_if_enabled("DURABILITY"));
311/// }
312/// ```
313#[must_use]
314pub fn immutable_if_enabled(policy_name: &'static str) -> DdsError {
315    DdsError::ImmutablePolicy {
316        policy: policy_name,
317    }
318}
319
320#[cfg(test)]
321#[allow(clippy::expect_used)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn entity_state_starts_disabled() {
327        let s = EntityState::new();
328        assert!(!s.is_enabled());
329    }
330
331    #[test]
332    fn entity_state_factory_starts_enabled() {
333        let s = EntityState::new_enabled();
334        assert!(s.is_enabled());
335    }
336
337    #[test]
338    fn enable_is_idempotent_and_reports_first_transition() {
339        let s = EntityState::new();
340        assert!(s.enable(), "first enable returns true");
341        assert!(!s.enable(), "second enable returns false");
342        assert!(s.is_enabled());
343    }
344
345    #[test]
346    fn instance_handles_are_unique_per_entity() {
347        let a = EntityState::new();
348        let b = EntityState::new();
349        assert_ne!(a.instance_handle(), b.instance_handle());
350    }
351
352    #[test]
353    fn status_bits_or_in_and_clear() {
354        let s = EntityState::new();
355        s.set_status_bits(0b0011);
356        s.set_status_bits(0b1100);
357        assert_eq!(s.status_changes(), 0b1111);
358        s.clear_status_changes(0b0101);
359        assert_eq!(s.status_changes(), 0b1010);
360    }
361
362    #[test]
363    fn status_condition_trigger_value() {
364        let s = EntityState::new();
365        let cond = StatusCondition::new(s.clone());
366        cond.set_enabled_statuses(0b1010);
367
368        // No status change → no trigger.
369        assert!(!cond.trigger_value());
370
371        // Status with a non-enabled bit → no trigger.
372        s.set_status_bits(0b0001);
373        assert!(!cond.trigger_value());
374
375        // Status with an enabled bit → trigger.
376        s.set_status_bits(0b0010);
377        assert!(cond.trigger_value());
378    }
379
380    #[test]
381    fn listener_mask_is_round_tripped() {
382        let s = EntityState::new();
383        s.set_listener_mask(0xABCD);
384        assert_eq!(s.listener_mask(), 0xABCD);
385    }
386
387    #[test]
388    fn immutable_if_enabled_returns_correct_error() {
389        let e = immutable_if_enabled("DURABILITY");
390        assert!(matches!(
391            e,
392            DdsError::ImmutablePolicy {
393                policy: "DURABILITY"
394            }
395        ));
396    }
397
398    // ---- §2.2.1.1.5 ALREADY_DELETED ----
399
400    #[test]
401    fn check_not_deleted_passes_for_fresh_entity() {
402        let s = EntityState::new();
403        assert!(s.check_not_deleted().is_ok());
404        assert!(!s.is_deleted());
405    }
406
407    #[test]
408    fn check_not_deleted_returns_already_deleted_after_mark() {
409        let s = EntityState::new();
410        let first = s.mark_deleted();
411        assert!(first, "first mark_deleted should return true");
412        assert!(s.is_deleted());
413        let res = s.check_not_deleted();
414        assert!(matches!(res, Err(DdsError::AlreadyDeleted)));
415    }
416
417    #[test]
418    fn mark_deleted_is_idempotent() {
419        let s = EntityState::new();
420        assert!(s.mark_deleted());
421        // Second call returns false (already-deleted state).
422        assert!(!s.mark_deleted());
423        assert!(s.is_deleted());
424    }
425
426    // ---- §2.2.1.1.7 NOT_ENABLED ----
427
428    #[test]
429    fn check_enabled_returns_not_enabled_for_disabled_entity() {
430        let s = EntityState::new();
431        assert!(!s.is_enabled());
432        let res = s.check_enabled();
433        assert!(matches!(res, Err(DdsError::NotEnabled)));
434    }
435
436    #[test]
437    fn check_enabled_passes_after_enable() {
438        let s = EntityState::new();
439        let _ = s.enable();
440        assert!(s.check_enabled().is_ok());
441    }
442
443    #[test]
444    fn check_enabled_passes_for_factory_entity() {
445        // DomainParticipantFactory is always enabled (Spec §2.2.2.1.4).
446        let s = EntityState::new_enabled();
447        assert!(s.check_enabled().is_ok());
448    }
449
450    // ---- §2.2.2.1.9 StatusCondition.get_entity ----
451
452    #[test]
453    fn status_condition_get_entity_handle_matches_owner_state() {
454        let state = EntityState::new();
455        let cond = StatusCondition::new(state.clone());
456        // Handle of the condition == handle of the entity it is bound to.
457        assert_eq!(cond.get_entity_handle(), state.instance_handle());
458    }
459
460    #[test]
461    fn status_condition_get_entity_handle_unique_per_entity() {
462        // Two different entities → two different handles via their
463        // StatusConditions.
464        let s1 = EntityState::new();
465        let s2 = EntityState::new();
466        let c1 = StatusCondition::new(s1);
467        let c2 = StatusCondition::new(s2);
468        assert_ne!(c1.get_entity_handle(), c2.get_entity_handle());
469    }
470
471    #[test]
472    fn status_condition_entity_state_returns_same_arc() {
473        let state = EntityState::new();
474        let cond = StatusCondition::new(state.clone());
475        // Identity via Arc::ptr_eq — the condition holds exactly this
476        // Arc, not a clone of the inner.
477        assert!(Arc::ptr_eq(&state, cond.entity_state()));
478    }
479
480    #[test]
481    fn status_condition_entity_state_reflects_lifecycle_changes() {
482        // The get_entity path must make lifecycle changes visible
483        // (e.g. enable, mark_deleted) so callers can inspect the state
484        // directly.
485        let state = EntityState::new();
486        let cond = StatusCondition::new(state.clone());
487        assert!(!cond.entity_state().is_enabled());
488        let _ = state.enable();
489        assert!(cond.entity_state().is_enabled());
490        let _ = state.mark_deleted();
491        assert!(cond.entity_state().is_deleted());
492    }
493}