Skip to main content

zerodds_dcps/
instance_handle.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `InstanceHandle` — opaker, lokaler Identifier fuer Entities und
4//! Sample-Instanzen (DDS DCPS 1.4 §2.3.3 IDL-PSM, §2.2.2.5.1
5//! SampleInfo.instance_handle).
6//!
7//! In der Spec ist `InstanceHandle_t` ein Builtin-Type ohne fixe
8//! Wire-Form — er wird **nie** auf den Wire gestellt, sondern dient
9//! ausschliesslich der lokalen Identifikation (z.B. um in
10//! `DataReader::read_instance()` einen Sample-Stream zu adressieren).
11//! Die Spec stellt nur sicher, dass `HANDLE_NIL` einen reservierten
12//! "kein Handle"-Wert hat.
13//!
14//! Wir kodieren ihn als **opake `u64`**:
15//! * Eindeutig pro Entity / Sample-Instanz innerhalb einer Runtime.
16//! * `HANDLE_NIL` = 0 (Spec-Konvention).
17//! * Erzeugung via [`InstanceHandleAllocator`] mit monoton steigendem
18//!   Counter — keine Wiederverwendung gedroppter Handles.
19
20extern crate alloc;
21
22use core::sync::atomic::{AtomicU64, Ordering};
23
24use zerodds_rtps::wire_types::Guid;
25
26/// Opaker `InstanceHandle_t` (DDS-DCPS 1.4 §2.3.3).
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
28pub struct InstanceHandle(u64);
29
30impl InstanceHandle {
31    /// Reservierter "kein Handle"-Wert (Spec-Konvention).
32    pub const NIL: Self = Self(0);
33
34    /// Konstruktor aus einem rohen u64-Wert. Niedrige API — ueblicherweise
35    /// erzeugt der [`InstanceHandleAllocator`] die Werte.
36    #[must_use]
37    pub const fn from_raw(raw: u64) -> Self {
38        Self(raw)
39    }
40
41    /// Roher Wert.
42    #[must_use]
43    pub const fn as_raw(&self) -> u64 {
44        self.0
45    }
46
47    /// `true` wenn der Handle [`Self::NIL`] ist.
48    #[must_use]
49    pub const fn is_nil(&self) -> bool {
50        self.0 == 0
51    }
52
53    /// Deterministische Ableitung eines [`InstanceHandle`] aus einem
54    /// `Guid` (16 Byte BuiltinTopicKey). Wird fuer
55    /// `ignore_*`-/`get_discovered_*`-APIs (DDS DCPS 1.4
56    /// §2.2.2.2.1.14-17, §2.2.2.2.1.27-30) verwendet, weil die Spec
57    /// dort `InstanceHandle_t` als Argument verlangt, der Builtin-
58    /// Subscriber aber `BuiltinTopicKey_t` (=Guid) als Sample-Key
59    /// fuehrt.
60    ///
61    /// Implementation: 64-bit FNV-1a ueber alle 16 Bytes — schnell,
62    /// no_std, kollisionsarm fuer einige tausend Discovered Entities.
63    /// `HANDLE_NIL` wird durch Anheben auf 1 vermieden.
64    #[must_use]
65    pub fn from_guid(guid: Guid) -> Self {
66        const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
67        const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
68        let mut h = FNV_OFFSET;
69        for b in guid.to_bytes() {
70            h ^= u64::from(b);
71            h = h.wrapping_mul(FNV_PRIME);
72        }
73        if h == 0 { Self(1) } else { Self(h) }
74    }
75}
76
77impl core::fmt::Display for InstanceHandle {
78    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79        if self.is_nil() {
80            f.write_str("HANDLE_NIL")
81        } else {
82            write!(f, "InstanceHandle({:#x})", self.0)
83        }
84    }
85}
86
87/// Spec-Alias fuer den NIL-Handle (DDS-DCPS 1.4 §2.3.3).
88pub const HANDLE_NIL: InstanceHandle = InstanceHandle::NIL;
89
90/// Atomarer Counter zur Vergabe eindeutiger [`InstanceHandle`]-Werte.
91///
92/// Eine Instanz pro Runtime/Process — ueblicherweise auf der
93/// `DcpsRuntime` als gemeinsame Allokationsquelle fuer Entities und
94/// Sample-Instanzen.
95#[derive(Debug)]
96pub struct InstanceHandleAllocator {
97    next: AtomicU64,
98}
99
100impl Default for InstanceHandleAllocator {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl InstanceHandleAllocator {
107    /// Neuer Allocator. Erste Vergabe liefert `1` (HANDLE_NIL=0 ist
108    /// reserviert).
109    #[must_use]
110    pub const fn new() -> Self {
111        Self {
112            next: AtomicU64::new(1),
113        }
114    }
115
116    /// Liefert den naechsten freien Handle (monoton steigend).
117    /// Thread-safe.
118    #[must_use]
119    pub fn allocate(&self) -> InstanceHandle {
120        let v = self.next.fetch_add(1, Ordering::Relaxed);
121        // Wraparound-Defense: u64 reicht > 580 Jahre bei 1 Mio Handles/s.
122        // Falls trotzdem 0 → NIL → wir ueberspringen und nehmen den
123        // naechsten.
124        if v == 0 {
125            self.allocate()
126        } else {
127            InstanceHandle(v)
128        }
129    }
130}
131
132#[cfg(test)]
133#[allow(clippy::expect_used)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn handle_nil_is_zero() {
139        assert_eq!(HANDLE_NIL.as_raw(), 0);
140        assert!(HANDLE_NIL.is_nil());
141    }
142
143    #[test]
144    fn allocator_starts_at_one() {
145        let alloc = InstanceHandleAllocator::new();
146        let h = alloc.allocate();
147        assert_eq!(h.as_raw(), 1);
148        assert!(!h.is_nil());
149    }
150
151    #[test]
152    fn allocator_is_monotonic() {
153        let alloc = InstanceHandleAllocator::new();
154        let h1 = alloc.allocate();
155        let h2 = alloc.allocate();
156        let h3 = alloc.allocate();
157        assert!(h1.as_raw() < h2.as_raw());
158        assert!(h2.as_raw() < h3.as_raw());
159    }
160
161    #[test]
162    fn handles_are_distinct() {
163        let alloc = InstanceHandleAllocator::new();
164        let mut seen = alloc::collections::BTreeSet::new();
165        for _ in 0..1000 {
166            let h = alloc.allocate();
167            assert!(seen.insert(h.as_raw()));
168        }
169    }
170
171    #[test]
172    fn display_shows_nil_or_hex() {
173        assert_eq!(alloc::format!("{HANDLE_NIL}"), "HANDLE_NIL");
174        let h = InstanceHandle::from_raw(0xABCD);
175        assert_eq!(alloc::format!("{h}"), "InstanceHandle(0xabcd)");
176    }
177
178    #[test]
179    fn from_raw_and_as_raw_roundtrip() {
180        let h = InstanceHandle::from_raw(42);
181        assert_eq!(h.as_raw(), 42);
182    }
183
184    #[test]
185    fn from_guid_is_deterministic() {
186        use zerodds_rtps::wire_types::Guid;
187        let g = Guid::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 0, 1, 0xC1]);
188        let h1 = InstanceHandle::from_guid(g);
189        let h2 = InstanceHandle::from_guid(g);
190        assert_eq!(h1, h2);
191        assert!(!h1.is_nil());
192    }
193
194    #[test]
195    fn from_guid_distinguishes_keys() {
196        use zerodds_rtps::wire_types::Guid;
197        let g1 = Guid::from_bytes([1; 16]);
198        let g2 = Guid::from_bytes([2; 16]);
199        assert_ne!(InstanceHandle::from_guid(g1), InstanceHandle::from_guid(g2));
200    }
201}