zerodds_iceoryx_cyclone/lib.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Crate `zerodds-iceoryx-cyclone`. Safety classification: **STANDARD**
3//! (FFI bridge over `libiceoryx_binding_c`; unsafe is reviewed and bounded).
4//!
5//! ZeroDDS ↔ Cyclone DDS same-host zero-copy bridge over the **iceoryx C++
6//! (POSH)** shared-memory transport — the SHM stack Cyclone uses through its
7//! `iox_psmx` plugin (with the `iox-roudi` daemon), as opposed to the
8//! iceoryx2-Rust stack `zerodds_flatdata` bridges to natively.
9//!
10//! This crate FFI-binds the stable `libiceoryx_binding_c` C ABI (no C++ name
11//! mangling) and speaks Cyclone's PSMX chunk convention directly, so a ZeroDDS
12//! process can READ a Cyclone-published sample and WRITE one a Cyclone reader
13//! consumes — true cross-vendor same-host zero-copy.
14//!
15//! Wire facts (reverse-engineered + live-verified, see
16//! `docs/interop/cyclone-iceoryx-shm-ground-truth.md`):
17//! * iceoryx ServiceDescription `{ "DDS_CYCLONE", <type-name>, ".<topic>" }`;
18//! * each chunk's iceoryx user-header is a [`DdsPsmxMetadata`], the user payload
19//! the sample (raw native struct for a self-contained type — `RAW_DATA`).
20//!
21//! Linux + iceoryx-POSH only: the FFI compiles under
22//! `cfg(all(target_os = "linux", feature = "posh"))` and links
23//! `libiceoryx_binding_c`. Without the feature the crate exposes only the
24//! representation-level [`DdsPsmxMetadata`] type so it builds anywhere.
25
26#![cfg_attr(not(feature = "posh"), allow(dead_code))]
27
28/// Cyclone `dds_psmx_metadata_t` (`dds/ddsc/dds_psmx.h`) — the iceoryx
29/// user-header carried alongside every PSMX sample. Field sizes are the
30/// live-verified layout: `instance_id` is `u32` (not `u64`), which is what
31/// places `sample_size` at the correct offset.
32#[repr(C)]
33#[derive(Clone, Copy, Debug)]
34pub struct DdsPsmxMetadata {
35 /// `dds_loaned_sample_state_t` enum: `2` == `RAW_DATA` (full raw sample).
36 pub sample_state: u32,
37 /// Reserved, must be `0`.
38 pub data_type: u32,
39 /// `dds_psmx_instance_id_t` (`u32`).
40 pub instance_id: u32,
41 /// Byte length of the user payload.
42 pub sample_size: u32,
43 /// Original writer GUID.
44 pub guid: [u8; 16],
45 /// Source timestamp (ns).
46 pub timestamp: i64,
47 /// DDSI status info (write / dispose / unregister).
48 pub statusinfo: u32,
49 /// RTPS encoding id (`NATIVE` when the payload is unserialized).
50 pub cdr_identifier: u16,
51 /// CDR options (`0` when unserialized).
52 pub cdr_options: u16,
53}
54
55impl DdsPsmxMetadata {
56 /// `dds_loaned_sample_state_t::DDS_LOANED_SAMPLE_STATE_RAW_DATA`.
57 pub const STATE_RAW_DATA: u32 = 2;
58 /// `dds_loaned_sample_state_t::DDS_LOANED_SAMPLE_STATE_SERIALIZED_DATA`.
59 pub const STATE_SERIALIZED_DATA: u32 = 4;
60 /// `cdr_identifier` value for classic CDR little-endian (`CDR_LE = 0x0001`
61 /// stored in Cyclone's host-order field). This is what Cyclone stamps on a
62 /// serialized iceoryx sample — observed live.
63 pub const CDR_LE: u16 = 0x0100;
64
65 /// A metadata header for a **serialized** sample of `size` bytes (classic
66 /// CDR / XCDR1 little-endian) from writer `guid` — what Cyclone's `iox_psmx`
67 /// writes for a non-self-contained type. Pair with `T::encode_xcdr1`.
68 pub fn serialized(size: u32, guid: [u8; 16]) -> Self {
69 Self {
70 sample_state: Self::STATE_SERIALIZED_DATA,
71 data_type: 0,
72 instance_id: 0,
73 sample_size: size,
74 guid,
75 timestamp: 0,
76 statusinfo: 0,
77 cdr_identifier: Self::CDR_LE,
78 cdr_options: 0,
79 }
80 }
81
82 /// `true` if the payload is a serialized (CDR) sample.
83 pub fn is_serialized(&self) -> bool {
84 self.sample_state == Self::STATE_SERIALIZED_DATA
85 }
86
87 /// A metadata header for a raw self-contained sample of `size` bytes from
88 /// writer `guid` — what Cyclone's `iox_psmx` writes for a fixed type.
89 pub fn raw(size: u32, guid: [u8; 16]) -> Self {
90 Self {
91 sample_state: Self::STATE_RAW_DATA,
92 data_type: 0,
93 instance_id: 0,
94 sample_size: size,
95 guid,
96 timestamp: 0,
97 statusinfo: 0,
98 // 0x00c0 is the RTPS "native sample" marker Cyclone stamps on a
99 // raw, unserialized loan (observed in the live capture).
100 cdr_identifier: 0x00c0,
101 cdr_options: 0,
102 }
103 }
104
105 /// `true` if the payload is the raw, unserialized sample (native struct).
106 pub fn is_raw(&self) -> bool {
107 self.sample_state == Self::STATE_RAW_DATA
108 }
109}
110
111/// The fixed iceoryx service string Cyclone's `iox_psmx` registers under.
112pub const CYCLONE_IOX_SERVICE: &str = "DDS_CYCLONE";
113
114/// Builds the iceoryx event string for `topic` in `partition` the way Cyclone
115/// does: `"<partition>.<topic>"`, with a leading `"."` for the empty partition.
116pub fn iox_event_name(partition: &str, topic: &str) -> String {
117 format!("{partition}.{topic}")
118}
119
120#[cfg(all(target_os = "linux", feature = "posh"))]
121mod posh;
122#[cfg(all(target_os = "linux", feature = "posh"))]
123pub use posh::{init_runtime, CycloneIoxReader, CycloneIoxWriter};