Skip to main content

zerodds_dcps/
sample_bytes.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! `SampleBytes` — zero-copy byte container for reader-path samples.
5//!
6//! Spec: `docs/specs/zerodds-zero-copy-1.0.md` §6 Wave 2.
7//!
8//! # Background
9//!
10//! `UserSample::Alive::payload` was historically a `Vec<u8>`. That forced
11//! `strip_user_encap` to cut off the encap-header offset via
12//! `payload[off..].to_vec()` — one heap alloc + copy per alive sample.
13//!
14//! `SampleBytes` replaces that Vec with an `Arc<[u8]>` + range. Strip
15//! operations become pure index arithmetic (`Arc::clone` is a refcount
16//! bump, not a copy). Heap allocs are eliminated on the hot path.
17//!
18//! # API
19//!
20//! - [`SampleBytes::from_vec`] — heap-Vec input (backward compat).
21//! - [`SampleBytes::from_arc_slice`] — Arc + range (zero-copy path).
22//! - [`SampleBytes::as_slice`] — slice read without copy.
23//! - [`SampleBytes::to_vec`] — materialization at an FFI boundary.
24//! - [`SampleBytes::len`] / `is_empty` — standard container API.
25//! - `Clone` is O(1) — just an Arc refcount bump.
26
27use alloc::sync::Arc;
28use alloc::vec::Vec;
29use core::ops::Range;
30
31/// Refcounted byte container for reader-path samples.
32///
33/// Allows zero-copy slicing of the wire bytes by tracking a range over
34/// an `Arc<[u8]>`. Clone is O(1).
35#[derive(Debug, Clone)]
36pub struct SampleBytes {
37    /// Refcounted data — typically comes from RTPS `DeliveredSample::payload`.
38    data: Arc<[u8]>,
39    /// View range over `data`. Strip operations only shift the range.
40    range: Range<usize>,
41}
42
43impl SampleBytes {
44    /// Constructs from an owned `Vec<u8>`. Heap alloc for the Arc wrap.
45    /// Use for test sample injection or when the bytes were freshly
46    /// allocated anyway.
47    #[must_use]
48    pub fn from_vec(v: Vec<u8>) -> Self {
49        let len = v.len();
50        Self {
51            data: Arc::from(v.into_boxed_slice()),
52            range: 0..len,
53        }
54    }
55
56    /// Constructs from an `Arc<[u8]>` with the full range. **Zero-copy** —
57    /// only the refcount is incremented.
58    #[must_use]
59    pub fn from_arc(data: Arc<[u8]>) -> Self {
60        let len = data.len();
61        Self {
62            data,
63            range: 0..len,
64        }
65    }
66
67    /// Constructs with an explicit range over an `Arc<[u8]>`. **Zero-copy**.
68    ///
69    /// # Panics
70    /// If `range.end > data.len()` or `range.start > range.end`.
71    #[must_use]
72    pub fn from_arc_slice(data: Arc<[u8]>, range: Range<usize>) -> Self {
73        assert!(
74            range.end <= data.len() && range.start <= range.end,
75            "SampleBytes range out of bounds"
76        );
77        Self { data, range }
78    }
79
80    /// Creates a new `SampleBytes` with the given sub-range relative to
81    /// the current view. **Zero-copy** — refcount bump.
82    ///
83    /// # Panics
84    /// If `sub.end > self.len()`.
85    #[must_use]
86    pub fn slice(&self, sub: Range<usize>) -> Self {
87        assert!(sub.end <= self.len(), "SampleBytes::slice out of bounds");
88        let start = self.range.start + sub.start;
89        let end = self.range.start + sub.end;
90        Self {
91            data: Arc::clone(&self.data),
92            range: start..end,
93        }
94    }
95
96    /// Current view as `&[u8]`. No copy.
97    #[must_use]
98    pub fn as_slice(&self) -> &[u8] {
99        &self.data[self.range.clone()]
100    }
101
102    /// Number of bytes in the current view.
103    #[must_use]
104    pub fn len(&self) -> usize {
105        self.range.end - self.range.start
106    }
107
108    /// `true` if the view is empty.
109    #[must_use]
110    pub fn is_empty(&self) -> bool {
111        self.range.is_empty()
112    }
113
114    /// Materializes the view into a `Vec<u8>`. **Copies** — use only at
115    /// FFI boundaries where owned data must be handed to C/Python/JS.
116    #[must_use]
117    pub fn to_vec(&self) -> Vec<u8> {
118        self.as_slice().to_vec()
119    }
120}
121
122impl AsRef<[u8]> for SampleBytes {
123    fn as_ref(&self) -> &[u8] {
124        self.as_slice()
125    }
126}
127
128impl core::ops::Deref for SampleBytes {
129    type Target = [u8];
130    fn deref(&self) -> &[u8] {
131        self.as_slice()
132    }
133}
134
135impl PartialEq for SampleBytes {
136    fn eq(&self, other: &Self) -> bool {
137        self.as_slice() == other.as_slice()
138    }
139}
140
141impl Eq for SampleBytes {}
142
143impl From<Vec<u8>> for SampleBytes {
144    fn from(v: Vec<u8>) -> Self {
145        Self::from_vec(v)
146    }
147}
148
149impl From<Arc<[u8]>> for SampleBytes {
150    fn from(a: Arc<[u8]>) -> Self {
151        Self::from_arc(a)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn from_vec_roundtrip() {
161        let s = SampleBytes::from_vec(alloc::vec![1, 2, 3, 4]);
162        assert_eq!(s.len(), 4);
163        assert_eq!(s.as_slice(), &[1, 2, 3, 4]);
164        assert_eq!(s.to_vec(), alloc::vec![1, 2, 3, 4]);
165    }
166
167    #[test]
168    fn from_arc_full_range() {
169        let arc: Arc<[u8]> = Arc::from(alloc::vec![10, 20, 30].into_boxed_slice());
170        let s = SampleBytes::from_arc(arc);
171        assert_eq!(s.as_slice(), &[10, 20, 30]);
172    }
173
174    #[test]
175    fn slice_is_zero_copy() {
176        let arc: Arc<[u8]> = Arc::from(alloc::vec![1, 2, 3, 4, 5].into_boxed_slice());
177        let s = SampleBytes::from_arc(arc);
178        let inner_ptr_before = s.as_slice().as_ptr() as usize;
179        let sub = s.slice(2..5);
180        let inner_ptr_after = sub.as_slice().as_ptr() as usize;
181        // Pointer 2 bytes further along: same Arc contents, just an offset.
182        assert_eq!(inner_ptr_after - inner_ptr_before, 2);
183        assert_eq!(sub.as_slice(), &[3, 4, 5]);
184    }
185
186    #[test]
187    fn nested_slice_offsets_compose() {
188        let s = SampleBytes::from_vec(alloc::vec![0, 1, 2, 3, 4, 5, 6, 7]);
189        let s1 = s.slice(2..7); // [2,3,4,5,6]
190        let s2 = s1.slice(1..4); // [3,4,5]
191        assert_eq!(s2.as_slice(), &[3, 4, 5]);
192    }
193
194    #[test]
195    fn clone_is_refcount_bump() {
196        let s = SampleBytes::from_vec(alloc::vec![1, 2, 3]);
197        let p1 = s.as_slice().as_ptr();
198        let s2 = s.clone();
199        let p2 = s2.as_slice().as_ptr();
200        assert_eq!(p1, p2, "Clone must share backing storage");
201    }
202
203    #[test]
204    fn empty_after_full_strip() {
205        let s = SampleBytes::from_vec(alloc::vec![1, 2, 3]);
206        let empty = s.slice(3..3);
207        assert!(empty.is_empty());
208    }
209
210    #[test]
211    #[should_panic(expected = "out of bounds")]
212    fn slice_oob_panics() {
213        let s = SampleBytes::from_vec(alloc::vec![1, 2]);
214        let _ = s.slice(0..5);
215    }
216}