Skip to main content

zerodds_dcps/
instance_handle.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `InstanceHandle` — opaque, local identifier for entities and sample
4//! instances (DDS DCPS 1.4 §2.3.3 IDL-PSM, §2.2.2.5.1
5//! SampleInfo.instance_handle).
6//!
7//! In the spec, `InstanceHandle_t` is a builtin type with no fixed wire
8//! form — it is **never** placed on the wire, but serves solely for
9//! local identification (e.g. to address a sample stream in
10//! `DataReader::read_instance()`). The spec only guarantees that
11//! `HANDLE_NIL` has a reserved "no handle" value.
12//!
13//! We encode it as an **opaque `u64`**:
14//! * Unique per entity / sample instance within a runtime.
15//! * `HANDLE_NIL` = 0 (spec convention).
16//! * Created via [`InstanceHandleAllocator`] with a monotonically
17//!   increasing counter — no reuse of dropped handles.
18
19extern crate alloc;
20
21use core::sync::atomic::{AtomicU64, Ordering};
22
23use zerodds_rtps::wire_types::Guid;
24
25/// Opaque `InstanceHandle_t` (DDS-DCPS 1.4 §2.3.3).
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
27pub struct InstanceHandle(u64);
28
29impl InstanceHandle {
30    /// Reserved "no handle" value (spec convention).
31    pub const NIL: Self = Self(0);
32
33    /// Constructor from a raw u64 value. Low-level API — normally the
34    /// [`InstanceHandleAllocator`] produces the values.
35    #[must_use]
36    pub const fn from_raw(raw: u64) -> Self {
37        Self(raw)
38    }
39
40    /// Raw value.
41    #[must_use]
42    pub const fn as_raw(&self) -> u64 {
43        self.0
44    }
45
46    /// `true` if the handle is [`Self::NIL`].
47    #[must_use]
48    pub const fn is_nil(&self) -> bool {
49        self.0 == 0
50    }
51
52    /// Deterministic derivation of an [`InstanceHandle`] from a `Guid`
53    /// (16-byte BuiltinTopicKey). Used for the `ignore_*` /
54    /// `get_discovered_*` APIs (DDS DCPS 1.4 §2.2.2.2.1.14-17,
55    /// §2.2.2.2.1.27-30), because there the spec requires
56    /// `InstanceHandle_t` as the argument, while the builtin subscriber
57    /// keeps `BuiltinTopicKey_t` (= Guid) as the sample key.
58    ///
59    /// Implementation: 64-bit FNV-1a over all 16 bytes — fast, no_std,
60    /// low collision for a few thousand discovered entities.
61    /// `HANDLE_NIL` is avoided by bumping it up to 1.
62    #[must_use]
63    pub fn from_guid(guid: Guid) -> Self {
64        const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
65        const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
66        let mut h = FNV_OFFSET;
67        for b in guid.to_bytes() {
68            h ^= u64::from(b);
69            h = h.wrapping_mul(FNV_PRIME);
70        }
71        if h == 0 { Self(1) } else { Self(h) }
72    }
73}
74
75impl core::fmt::Display for InstanceHandle {
76    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
77        if self.is_nil() {
78            f.write_str("HANDLE_NIL")
79        } else {
80            write!(f, "InstanceHandle({:#x})", self.0)
81        }
82    }
83}
84
85/// Spec alias for the NIL handle (DDS-DCPS 1.4 §2.3.3).
86pub const HANDLE_NIL: InstanceHandle = InstanceHandle::NIL;
87
88/// Atomic counter for handing out unique [`InstanceHandle`] values.
89///
90/// One instance per runtime/process — typically on the `DcpsRuntime`
91/// as the shared allocation source for entities and sample instances.
92#[derive(Debug)]
93pub struct InstanceHandleAllocator {
94    next: AtomicU64,
95}
96
97impl Default for InstanceHandleAllocator {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl InstanceHandleAllocator {
104    /// New allocator. The first allocation returns `1` (HANDLE_NIL=0 is
105    /// reserved).
106    #[must_use]
107    pub const fn new() -> Self {
108        Self {
109            next: AtomicU64::new(1),
110        }
111    }
112
113    /// Returns the next free handle (monotonically increasing).
114    /// Thread-safe.
115    #[must_use]
116    pub fn allocate(&self) -> InstanceHandle {
117        let v = self.next.fetch_add(1, Ordering::Relaxed);
118        // Wraparound defense: u64 lasts > 580 years at 1M handles/s.
119        // If it still hits 0 → NIL → we skip it and take the next one.
120        if v == 0 {
121            self.allocate()
122        } else {
123            InstanceHandle(v)
124        }
125    }
126}
127
128#[cfg(test)]
129#[allow(clippy::expect_used)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn handle_nil_is_zero() {
135        assert_eq!(HANDLE_NIL.as_raw(), 0);
136        assert!(HANDLE_NIL.is_nil());
137    }
138
139    #[test]
140    fn allocator_starts_at_one() {
141        let alloc = InstanceHandleAllocator::new();
142        let h = alloc.allocate();
143        assert_eq!(h.as_raw(), 1);
144        assert!(!h.is_nil());
145    }
146
147    #[test]
148    fn allocator_is_monotonic() {
149        let alloc = InstanceHandleAllocator::new();
150        let h1 = alloc.allocate();
151        let h2 = alloc.allocate();
152        let h3 = alloc.allocate();
153        assert!(h1.as_raw() < h2.as_raw());
154        assert!(h2.as_raw() < h3.as_raw());
155    }
156
157    #[test]
158    fn handles_are_distinct() {
159        let alloc = InstanceHandleAllocator::new();
160        let mut seen = alloc::collections::BTreeSet::new();
161        for _ in 0..1000 {
162            let h = alloc.allocate();
163            assert!(seen.insert(h.as_raw()));
164        }
165    }
166
167    #[test]
168    fn display_shows_nil_or_hex() {
169        assert_eq!(alloc::format!("{HANDLE_NIL}"), "HANDLE_NIL");
170        let h = InstanceHandle::from_raw(0xABCD);
171        assert_eq!(alloc::format!("{h}"), "InstanceHandle(0xabcd)");
172    }
173
174    #[test]
175    fn from_raw_and_as_raw_roundtrip() {
176        let h = InstanceHandle::from_raw(42);
177        assert_eq!(h.as_raw(), 42);
178    }
179
180    #[test]
181    fn from_guid_is_deterministic() {
182        use zerodds_rtps::wire_types::Guid;
183        let g = Guid::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 0, 1, 0xC1]);
184        let h1 = InstanceHandle::from_guid(g);
185        let h2 = InstanceHandle::from_guid(g);
186        assert_eq!(h1, h2);
187        assert!(!h1.is_nil());
188    }
189
190    #[test]
191    fn from_guid_distinguishes_keys() {
192        use zerodds_rtps::wire_types::Guid;
193        let g1 = Guid::from_bytes([1; 16]);
194        let g2 = Guid::from_bytes([2; 16]);
195        assert_ne!(InstanceHandle::from_guid(g1), InstanceHandle::from_guid(g2));
196    }
197}