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}