Skip to main content

nros_node/
format_check.rs

1//! RFC-0088 / phase-421 W1 — the message-format check, as a compile error.
2//!
3//! ROS 2 names its serialization format with a string and answers
4//! `rmw_get_serialization_format()` at run time, because
5//! `rosidl_typesupport_c` resolves the format's implementation through
6//! `dlopen` and the string is the linker key. nano-ros links one image and
7//! selects its backend by cargo feature, so the same question has a
8//! compile-time answer: [`crate::session::IMAGE_SERIALIZATION_FORMAT_ID`].
9//!
10//! [`assert_message_format`] compares a message's declared
11//! [`nros_core::RosMessage::SERIALIZATION_FORMAT_ID`] against that constant inside an
12//! inline `const {}` block. The comparison therefore happens during
13//! monomorphisation of the entity-creation call, and costs nothing at run
14//! time — no branch appears on the publish path, which is the property
15//! RFC-0088 D1 asks for.
16//!
17//! # What the error looks like
18//!
19//! Creating a publisher for a `Uorb` message in an image whose backend speaks
20//! CDR fails like this. The primary span is the `assert!` below and the
21//! offending type + call site arrive as notes, which is how rustc reports a
22//! post-monomorphisation const failure:
23//!
24//! ```text
25//! error[E0080]: evaluation panicked: message serialization format does not
26//!               match the linked backend (RFC-0088)
27//!   --> packages/core/nros-node/src/format_check.rs:83:9
28//!    | evaluation of `format_check::assert_message_format::<UorbProbe>::{constant#0}`
29//!    | failed here
30//!
31//! note: the above error was encountered while instantiating
32//!       `fn assert_message_format::<UorbProbe>`
33//!   --> src/main.rs:12:9
34//!    |
35//! 12 |     node.create_publisher::<VehicleStatus>("/status")?;
36//! ```
37//!
38//! **It is a `cargo build` error, not a `cargo check` one.** An inline `const`
39//! block in a generic function is evaluated by the monomorphisation collector,
40//! which only runs during codegen — `cargo check -p nros-node` compiles the
41//! mismatch silently. Measured 2026-09-04. `just ci gate` catches it because
42//! `test-unit` builds; a lane that only type-checks does not.
43//!
44//! # Coverage
45//!
46//! The check reads `nros_core::RosMessage::SERIALIZATION_FORMAT_ID`, and
47//! `MessageForRmw` — the bound every typed creator carries — requires
48//! `RosMessage` under **every** backend. So the assertion is universal: zenoh,
49//! XRCE, Cyclone and uORB alike.
50//!
51//! Keying it on `nros_serdes::schema::Message` instead would have covered only
52//! Cyclone, because `MessageForRmw` requires a schema solely under
53//! `cfg(rmw_needs_type_descriptors)` — and would therefore have been absent
54//! under uORB, the one backend whose format differs and the reason the check
55//! exists. The const is defaulted rather than required for the reason
56//! phase-380 W4 recorded: tightening the message contract to serve a build
57//! assertion broke `examples/native/rust/custom-msg`, the documented
58//! hand-written-message pattern. A default costs those implementors nothing.
59//!
60//! # Proving the negative case
61//!
62//! A compile error cannot be asserted by a running test, and this workspace has
63//! no `trybuild` (or any compile-fail) harness; adding one for a single case is
64//! more machinery than the case is worth. Reproduce it by hand instead —
65//! append to this file:
66//!
67//! ```ignore
68//! fn _mismatch() {
69//!     struct UorbProbe;
70//!     impl nros_core::RosMessage for UorbProbe {
71//!         const SERIALIZATION_FORMAT_ID = nros_serdes::format::SerializationFormatId::Uorb;
72//!         const TYPE_NAME: &'static str = "px4/msg/UorbProbe";
73//!         const TYPE_HASH: &'static str = "";
74//!     }
75//!     assert_message_format::<UorbProbe>();
76//! }
77//! ```
78//!
79//! The probe must be `pub` (or otherwise reachable): a private, never-called
80//! function is dropped before the monomorphisation collector runs, and the
81//! assertion then never instantiates — measured, having first written the
82//! probe private and seen a clean build.
83//!
84//! and `cargo build -p nros-node --features rmw-cffi` reports the `E0080`
85//! above (`cargo check` does not — see above). The runnable half
86//! of the claim is `tests::cdr_and_uorb_are_distinguishable`: if the two
87//! formats ever stopped differing, the compile error would stop being
88//! reachable and every assertion in the tree would pass vacuously.
89
90/// Assert at compile time that `M` is encoded in the format the linked backend
91/// speaks.
92///
93/// Zero-sized and inlined away; the whole effect is the `const {}` block, which
94/// is evaluated when this function is monomorphised for `M`. See the module
95/// docs for the error a mismatch produces and for what it does *not* cover.
96#[inline(always)]
97pub fn assert_message_format<M: nros_core::RosMessage>() {
98    const {
99        assert!(
100            <M as nros_core::RosMessage>::SERIALIZATION_FORMAT_ID.as_u8()
101                == crate::session::IMAGE_SERIALIZATION_FORMAT_ID.as_u8(),
102            "message serialization format does not match the linked backend (RFC-0088)"
103        );
104    }
105}
106
107/// The raw-entity counterpart: assert that `F`, the format a caller states its
108/// already-encoded bytes are in, is the one the linked backend speaks.
109///
110/// `EmbeddedRawPublisher::publish_raw` was documented as taking "raw
111/// CDR-encoded data (must include CDR header)" and checked by nothing. This is
112/// that sentence, as a bound a caller can name.
113///
114/// The raw constructors do **not** take `F` today: Rust forbids a default type
115/// parameter on a function (`invalid_type_param_default`, deny-by-default
116/// future-compat lint), so `create_publisher_raw<F = Cdr>` does not exist and
117/// making them generic without a default would break every existing call site's
118/// inference. A caller that wants the check states it explicitly:
119///
120/// ```
121/// # use nros_node::format_check::assert_raw_format;
122/// assert_raw_format::<nros_serdes::format::Cdr>();
123/// ```
124#[inline(always)]
125pub fn assert_raw_format<F: nros_serdes::format::SerializationFormat>() {
126    const {
127        assert!(
128            <F as nros_serdes::format::SerializationFormat>::ID.as_u8()
129                == crate::session::IMAGE_SERIALIZATION_FORMAT_ID.as_u8(),
130            "raw payload format does not match the linked backend (RFC-0088)"
131        );
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use nros_serdes::format::{Cdr, SerializationFormat, Uorb};
138
139    /// The assertion can only be load-bearing if the two formats it compares
140    /// are actually distinguishable. If this ever held, every
141    /// `assert_message_format` in the tree would pass vacuously.
142    #[test]
143    fn cdr_and_uorb_are_distinguishable() {
144        assert_ne!(
145            <Cdr as SerializationFormat>::ID.as_u8(),
146            <Uorb as SerializationFormat>::ID.as_u8(),
147            "Cdr and Uorb share a discriminant: the compile-time format check \
148             cannot fail, so it checks nothing"
149        );
150        assert_ne!(
151            <Cdr as SerializationFormat>::NAME,
152            <Uorb as SerializationFormat>::NAME
153        );
154    }
155
156    /// The positive case, exercised for real: a CDR message passes the same
157    /// assertion the entity creators run, against this image's backend
158    /// constant. If the constant or the message's declared format disagreed,
159    /// this test would fail to COMPILE — which is the intended failure mode.
160    #[test]
161    fn a_cdr_message_matches_this_image() {
162        struct Probe;
163        impl nros_serdes::Serialize for Probe {
164            fn serialize(
165                &self,
166                _w: &mut nros_serdes::cdr::CdrWriter,
167            ) -> Result<(), nros_serdes::error::SerError> {
168                Ok(())
169            }
170        }
171        impl nros_serdes::Deserialize for Probe {
172            fn deserialize(
173                _r: &mut nros_serdes::cdr::CdrReader,
174            ) -> Result<Self, nros_serdes::error::DeserError> {
175                Ok(Probe)
176            }
177        }
178        impl nros_core::RosMessage for Probe {
179            const TYPE_NAME: &'static str = "test_msgs/msg/Probe";
180            const TYPE_HASH: &'static str = "";
181        }
182
183        super::assert_message_format::<Probe>();
184        super::assert_raw_format::<Cdr>();
185
186        // The image this test runs in is a CDR image; state that, so a future
187        // backend swap is a failing assertion here rather than a silent
188        // inversion of what the two tests above mean.
189        assert_eq!(
190            crate::session::IMAGE_SERIALIZATION_FORMAT_ID.as_u8(),
191            <Cdr as SerializationFormat>::ID.as_u8()
192        );
193        assert_eq!(crate::session::IMAGE_SERIALIZATION_FORMAT, "cdr");
194    }
195}