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}