Skip to main content

nros_serdes/
format.rs

1//! Serialization format identity (RFC-0088, phase-421 W1).
2//!
3//! ROS 2 names its serialization format with a string because
4//! `rosidl_typesupport_c` resolves the format's implementation through
5//! `dlopen` — "if the identifier is the same as this handle's
6//! typesupport_identifier, then the handle is simply returned, otherwise it's
7//! loaded from a shared library". The string is a dynamic-linker key.
8//!
9//! nano-ros links one image, so the key can be a **type**. A message declares
10//! its format, a backend declares its format, and the two are compared at
11//! compile time. Nothing is resolved, dispatched or compared at run time on the
12//! publish path.
13//!
14//! # The discriminant is image-local; the string is not
15//!
16//! [`SerializationFormatId`] is a `u8` **assigned within one image**. nano-ros
17//! cannot allocate a globally unique discriminant to a third-party format, so
18//! the value is meaningful only inside the build that produced it. The
19//! `NAME` string is the identity that crosses image boundaries — the bridge
20//! config, tooling output, and the `get_serialization_format` vtable slot all
21//! carry the string, never the number.
22//!
23//! Treating the discriminant as global is the one mistake this module exists to
24//! prevent: two independently built images would disagree about what `3` means,
25//! which is a wire-visible bug with a compile-time-looking cause.
26
27/// Image-local discriminant for a serialization format.
28///
29/// In-tree formats hold low reserved values for readability. A third-party
30/// format declared by a provider package (RFC-0087 family `serdes`) is assigned
31/// a value by the build from the set of formats that image declares.
32#[repr(u8)]
33#[derive(Copy, Clone, PartialEq, Eq, Debug)]
34pub enum SerializationFormatId {
35    /// OMG CDR as ROS 2 puts it on the wire, including the encapsulation
36    /// header. Every DDS-derived backend and zenoh speak this.
37    Cdr = 1,
38    /// PX4's in-memory struct, verbatim — no encoding step at all (RFC-0011).
39    Uorb = 2,
40}
41
42impl SerializationFormatId {
43    /// The cross-image identity for this format.
44    pub const fn as_str(self) -> &'static str {
45        match self {
46            Self::Cdr => "cdr",
47            Self::Uorb => "uorb",
48        }
49    }
50
51    /// The raw discriminant, for a C ABI boundary or a one-byte comparison.
52    pub const fn as_u8(self) -> u8 {
53        self as u8
54    }
55}
56
57/// A serialization format, as a type.
58///
59/// Implementors are zero-sized markers. The trait carries no methods on
60/// purpose: it exists to be *named* in an associated type, so that a mismatch
61/// is a compile error rather than a runtime branch.
62pub trait SerializationFormat {
63    /// Image-local discriminant.
64    const ID: SerializationFormatId;
65    /// Cross-image identity. Always `ID.as_str()` for in-tree formats.
66    const NAME: &'static str;
67}
68
69/// ROS 2's wire encoding, and nano-ros's default.
70pub struct Cdr;
71
72impl SerializationFormat for Cdr {
73    const ID: SerializationFormatId = SerializationFormatId::Cdr;
74    const NAME: &'static str = "cdr";
75}
76
77/// PX4 uORB: the payload *is* the struct, so there is no encoding step.
78pub struct Uorb;
79
80impl SerializationFormat for Uorb {
81    const ID: SerializationFormatId = SerializationFormatId::Uorb;
82    const NAME: &'static str = "uorb";
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn name_agrees_with_discriminant() {
91        assert_eq!(Cdr::NAME, Cdr::ID.as_str());
92        assert_eq!(Uorb::NAME, Uorb::ID.as_str());
93    }
94
95    #[test]
96    fn discriminants_are_distinct_and_stable() {
97        assert_eq!(SerializationFormatId::Cdr.as_u8(), 1);
98        assert_eq!(SerializationFormatId::Uorb.as_u8(), 2);
99    }
100}