Skip to main content

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};