zerodds_flatdata/lib.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! FlatStruct trait + slot header for zero-copy same-host pub/sub.
4//!
5//! Crate `zerodds-flatdata`. Safety classification: **STANDARD**.
6//!
7//! Spec: `docs/specs/zerodds-flatdata-1.0.md`.
8//!
9//! # Safety rationale
10//!
11//! This crate implements an `unsafe trait FlatStruct` whose
12//! guarantees (Copy + repr(C) + 'static + no pointers/Vec/String) the
13//! implementer assures via `unsafe impl`. The `as_bytes` /
14//! `from_bytes_unchecked` helpers are then safe by layout — we
15//! localize the `unsafe` island here instead of scattering it into
16//! the DataWriter path.
17
18#![cfg_attr(not(feature = "std"), no_std)]
19#![warn(missing_docs)]
20// Rationale: the FlatStruct trait is unsafe by design (layout
21// guarantees are the caller's responsibility). Per-block SAFETY
22// comments are below.
23#![allow(unsafe_code)]
24
25#[cfg(feature = "alloc")]
26extern crate alloc;
27
28#[cfg(feature = "std")]
29mod allocator;
30#[cfg(feature = "std")]
31mod backend;
32#[cfg(feature = "iceoryx2-bridge")]
33mod iceoryx;
34#[cfg(feature = "alloc")]
35mod locator;
36#[cfg(feature = "posix-mmap")]
37mod posix;
38#[cfg(feature = "std")]
39mod pubsub;
40mod slot;
41
42#[cfg(feature = "std")]
43pub use allocator::{InMemorySlotAllocator, SlotError, SlotHandle};
44#[cfg(feature = "std")]
45pub use backend::SlotBackend;
46#[cfg(feature = "iceoryx2-bridge")]
47pub use iceoryx::{
48 Iceoryx2Error, Iceoryx2Publisher, Iceoryx2Subscriber, RawIceoryx2Publisher,
49 RawIceoryx2Subscriber,
50};
51#[cfg(feature = "alloc")]
52pub use locator::{LocatorError, ShmLocator, fnv1a_32, is_same_host};
53#[cfg(feature = "posix-mmap")]
54pub use posix::{PosixSlotAllocator, PosixSlotError};
55#[cfg(feature = "std")]
56pub use pubsub::{FlatReader, FlatSampleRef, FlatSlot, FlatWriter};
57pub use slot::{ReaderMask, SLOT_HEADER_SIZE, SlotHeader};
58
59/// Marker trait for FlatData-capable types.
60///
61/// Spec quote (zerodds-flatdata-1.0 §1.1):
62///
63/// > Guarantees:
64/// > - `Self: Copy` (no Drop glue, plain bytes)
65/// > - `Self: 'static` (no lifetime reference)
66/// > - `#[repr(C)]` with a fixed, defined alignment
67/// > - `as_bytes()` and `from_bytes_unchecked()` are safe by layout
68///
69/// # Safety
70///
71/// The implementer MUST ensure:
72/// - `Self` is `#[repr(C)]` (or `#[repr(transparent)]` over a
73/// single `repr(C)` type).
74/// - All fields are `FlatStruct` or primitive types without
75/// padding-sensitive UB (no `#[repr(packed)]` with pointer-aligned fields).
76/// - `Self: Copy` (the trait bound enforces this).
77/// - `TYPE_HASH` is unique for the exact wire-layout variant;
78/// on every schema change the hash MUST be regenerated.
79pub unsafe trait FlatStruct: Copy + 'static + Send + Sync {
80 /// Wire size of the `repr(C)` struct (= `core::mem::size_of::<Self>()`).
81 const WIRE_SIZE: usize = core::mem::size_of::<Self>();
82
83 /// Unique type-hash (16 byte). Caller code generates it via
84 /// SHA-256(`type_name + field_layout_string`) and takes the
85 /// first 16 byte. The reader checks this hash against the
86 /// discovery hash; on mismatch → slot drop.
87 const TYPE_HASH: [u8; 16];
88
89 /// Returns the slot layout as a slice. Safe by layout — `Self: Copy`
90 /// + `repr(C)` guarantee that the byte cast is defined.
91 #[must_use]
92 fn as_bytes(&self) -> &[u8] {
93 // SAFETY: the FlatStruct trait requires repr(C) + Copy. This makes
94 // `*const Self` ↔ `*const u8` a well-defined reinterpret
95 // cast (no tail-padding leaks because the caller has
96 // committed to that).
97 unsafe {
98 core::slice::from_raw_parts(core::ptr::from_ref(self).cast::<u8>(), Self::WIRE_SIZE)
99 }
100 }
101
102 /// Reconstructs from a raw slice. The caller MUST:
103 /// - bytes.len() >= WIRE_SIZE
104 /// - bytes provenance valid for WIRE_SIZE
105 /// - type-hash verified beforehand (otherwise UB on alignment mismatch)
106 ///
107 /// # Safety
108 /// See above.
109 #[must_use]
110 unsafe fn from_bytes_unchecked(bytes: &[u8]) -> Self {
111 debug_assert!(bytes.len() >= Self::WIRE_SIZE);
112 // SAFETY: caller contract + WIRE_SIZE check.
113 unsafe { core::ptr::read_unaligned(bytes.as_ptr().cast::<Self>()) }
114 }
115}
116
117#[cfg(test)]
118#[allow(clippy::expect_used, clippy::unwrap_used)]
119mod tests {
120 use super::*;
121
122 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
123 #[repr(C)]
124 struct Pose {
125 x: i64,
126 y: i64,
127 z: i64,
128 }
129
130 // SAFETY: Pose is repr(C), Copy, 'static. All fields are
131 // primitive i64 without padding issues.
132 unsafe impl FlatStruct for Pose {
133 const TYPE_HASH: [u8; 16] = [0x42; 16];
134 }
135
136 #[test]
137 fn wire_size_matches_size_of() {
138 assert_eq!(Pose::WIRE_SIZE, core::mem::size_of::<Pose>());
139 assert_eq!(Pose::WIRE_SIZE, 24);
140 }
141
142 #[test]
143 fn as_bytes_roundtrip() {
144 let p = Pose { x: 1, y: 2, z: 3 };
145 let bytes = p.as_bytes();
146 assert_eq!(bytes.len(), 24);
147 // SAFETY: bytes come from exactly this Pose instance, so the
148 // type-hash is trivially consistent.
149 let p2: Pose = unsafe { Pose::from_bytes_unchecked(bytes) };
150 assert_eq!(p, p2);
151 }
152
153 #[test]
154 fn type_hash_is_consistent() {
155 assert_eq!(Pose::TYPE_HASH, [0x42; 16]);
156 }
157}