nros_core/codegen_version.rs
1// RFC-0090 / phase-429 — the codegen version, the one token that says whether
2// generated code and this runtime can work together.
3//
4// NO `//!` INNER DOC COMMENTS IN THIS FILE, ON PURPOSE. It is `include!`d
5// verbatim by `nros-build-helpers`, and an inner doc comment inside an
6// `include!` expansion is a hard error (E0753). The module docs live on the
7// `pub mod` in `lib.rs`. Keep this file a dependency-free set of `const`s and
8// `const fn`s: anything else breaks the include.
9//
10// THREE READERS, THREE REASONS — do not "unify" them:
11//
12// * `nros-core` itself, directly.
13// * `nros-build-helpers` (defines the runtime anchors) and `rosidl-codegen`
14// (stamps the emitted artifacts), both by `include!`. Neither can afford a
15// `nros-core` dependency edge for a pair of `const`s: the first is host-only
16// and appears in every tracked leaf lockfile, and the second lives in the
17// separate `packages/cli` workspace, so the edge would add rows to its
18// lockfile too. `include!` costs nothing in the graph, and rustc records the
19// path in the depfile, so editing the constants rebuilds both.
20// * the CLI's guard, by parsing this file as TEXT. It inspects a CONSUMER's
21// tree at run time, where compiling is not available. This is the only
22// parser, and it exists because the other two options do not apply.
23
24/// The codegen version this runtime emits and accepts.
25///
26/// **Bump this deliberately** when the interface between generated code and the
27/// runtime changes: a trait signature generated code implements, a symbol it
28/// defines, a layout rule it obeys. Do NOT bump it for a cosmetic template
29/// edit — that moves the fingerprint, which is a different question (see the
30/// module docs).
31///
32/// Bumping invalidates every generated tree. That is affordable here because
33/// `generated/` is never committed (CLAUDE.md), so regeneration is always
34/// available — and it is exactly why the value must not move on cosmetics.
35///
36/// Gated by `check-codegen-version-surface`, which fails when the surface
37/// generated code names changes and this constant does not.
38pub const NROS_CODEGEN_VERSION: u32 = 18;
39
40/// The oldest codegen version this runtime still accepts.
41///
42/// Equal to [`NROS_CODEGEN_VERSION`] at introduction: no window, because
43/// `generated/` is never committed and regeneration is therefore always
44/// available. Raise the floor only when a real migration needs one, and lower
45/// it never.
46///
47/// Held at 1 while [`NROS_CODEGEN_VERSION`] moved to 2 (phase-417, rebased onto
48/// phase-429's gate). That surface move is ADDITIVE plus one relocation, so a
49/// tree generated against version 1 still runs: the C additions are new entry
50/// points, the two C++ changes add `size()`/`empty()` and `std::string` interop
51/// to `FixedString`/`HeapString`, and the four `rcl_ret_t` declarations that
52/// left `action.h` / `client.h` / `parameter.h` / `service.h` were CONSOLIDATED
53/// into `nros_generated.h`, which all four still reach through `rclc/types.h`
54/// — the headers say so in a comment, and keeping that include is what makes
55/// `#include <rclc/action.h>` continue to compile. Nothing a version-1 tree
56/// names was withdrawn, so this is the window the doc above describes rather
57/// than a migration.
58///
59/// Still 2 while [`NROS_CODEGEN_VERSION`] moved to 3 (issues 1148/1149). That
60/// move is one ADDED runtime entry point, `rclc_cdr_align`, which the
61/// header-only `rclc_cdr_borrow_le_slice_*` now call so an 8-byte borrowed
62/// sequence starts on its stream boundary. A version-2 tree names nothing that
63/// was withdrawn: it compiles against the corrected `rclc/detail/view.h` and picks the
64/// fix up for free, because the view helpers are `static inline` in the header
65/// rather than symbols the generated code defines.
66///
67/// Still 2 while [`NROS_CODEGEN_VERSION`] moved to 4 (phase-427 W7). That move
68/// is a SPELLING change and nothing else: `nros::bind_subscription_sized` — the
69/// one demanded C++ signature that names the node type — now writes its
70/// parameter `::rclcpp::Node&` where it wrote `Node&`, because the class moved
71/// to `rclcpp::` and `nros::Node` became a deprecated alias for it. The two
72/// spellings are ONE TYPE (`std::is_same<rclcpp::Node, nros::Node>` is asserted
73/// by `tests/compile/one_node_type.cpp`), so a version-3 tree — which declares
74/// its node `static ::nros::Node __nros_node_0;` — still compiles and still
75/// links, with a deprecation warning naming its replacement. Nothing it names
76/// was withdrawn, which is the window the doc above describes.
77///
78/// The version moves anyway because the gate is fail-closed on the TEXT of a
79/// demanded declaration and should stay that way: it cannot know that two
80/// spellings are one type, and a gate that tried to would be one that misses a
81/// real rename.
82///
83/// Still 2 while [`NROS_CODEGEN_VERSION`] moved to 5 (phase-417 W5.b/c/d). That
84/// move is ADDITIVE and, in most of its entries, is not a change to the runtime
85/// at all: the rclc preset constructors landed as `static inline` forwarders in
86/// `<nros/{publisher,subscription,service,client,action}.h>`, and naming a type
87/// inside one of those headers is what puts a `c|type|<header>|<name>` row in
88/// the extracted surface — `rclc_message_type_t`, `rcl_node_t`,
89/// `rclc_service_type_t` and `rclc_action_type_t` were already declared in
90/// `nros_generated.h` and still are, reached the same way. The one genuinely
91/// new declaration a demanded name resolves to is
92/// `rclc_service_init_with_qos`, which is an entry point that already existed
93/// and is now also visible from `<rclc/service.h>`. Nothing a version-4 tree
94/// names was withdrawn, so this is the window the doc above describes rather
95/// than a migration.
96///
97/// The version moves anyway for the reason the version-4 paragraph gives: the
98/// gate is fail-closed on the TEXT of a demanded declaration and should stay
99/// that way. A gate that tried to decide which additions are "really" runtime
100/// changes is one that misses a real withdrawal.
101///
102/// Still 2 while [`NROS_CODEGEN_VERSION`] moved to 6 (phase-417 W4.b). That
103/// move is a WITHDRAWAL, and it is the first one — `nros_core::action` stopped
104/// re-exporting `ActionServer` and `ActionClient`, two "type-level marker"
105/// structs. A version-5 tree still runs, because no generated tree has ever
106/// named them: measured before the deletion, they had ZERO references anywhere
107/// in this repository, and they were UNREACHABLE through the `nros` facade at
108/// all, which re-exports `nros_node`'s live `ActionServer` / `ActionClient`
109/// under exactly those spellings. So the floor stays where it is. The gate is
110/// fail-closed on the extracted surface and cannot know which names nothing
111/// reached, which is the property that makes it worth keeping.
112///
113/// Still 2 while [`NROS_CODEGEN_VERSION`] moved to 7 (issue 1437, phase-444).
114/// ADDITIVE on the surface generated code names, plus one APPEND to a struct
115/// nothing generated declares. The additions are six new C entry points
116/// (`nros_{publisher,subscription}_get_actual_qos` and the four service /
117/// client halves) and two enumerators on each of the four `nros_qos_*_t`
118/// enums (`UNKNOWN`, and `SYSTEM_DEFAULT` on three of them), appended so every
119/// existing discriminant keeps its value. The append is `_executor` on
120/// `rcl_subscription_t`, placed after `_opaque` so every existing field
121/// offset is unchanged — it grows the struct, which is why this is a surface
122/// move at all, and a version-6 tree that only names the struct by pointer is
123/// unaffected. Nothing was withdrawn, so the floor stays where it is.
124///
125/// The gate reports the move as two CHANGED types rather than as the four
126/// added prototypes, because a `const struct rcl_service_t *` parameter is a
127/// tagged reference and keys on the same `c|type|<header>|<name>` row as the
128/// struct itself. That is the gate being fail-closed on text, which is the
129/// property the version-4 paragraph above argues for keeping.
130///
131/// Still 2 while [`NROS_CODEGEN_VERSION`] moved to 8 (phase-467 Q3). ADDITIVE,
132/// and the first move that adds a TRAIT generated code implements — which is
133/// the trigger the doc above names first. `nros_core::SecNanosecMsg` is the
134/// Rust stand-in for the C++ `template <typename TimeMsgT> Time::to_msg`: Rust
135/// has no structural bound for "has `sec` and `nanosec`", so
136/// `rosidl-codegen` emits `impl nros_core::SecNanosecMsg for <Msg>` for every
137/// generated message with that exact field set, and
138/// `nros_core::Time::to_ros_msg()` returns through it. The surface moved by two
139/// rows, both additions: the trait, and the `pub use time::` re-export that
140/// publishes it.
141///
142/// A version-7 tree still runs: it names nothing that was withdrawn, and the
143/// impl is something a NEWER tree emits rather than something an older one is
144/// missing. What an older tree loses is only the conversion itself —
145/// `t.to_ros_msg()` into one of ITS types will not resolve until it is
146/// regenerated, which is a compile error at the call site on the day someone
147/// writes the call, not a silent mismatch. So the floor stays where it is.
148///
149/// Still 2 while [`NROS_CODEGEN_VERSION`] moved to 9 (issue 1293, phase-480
150/// W4). ADDITIVE: `nros_serdes::schema::EMPTY_STRUCT_MEMBER`, the name of
151/// rosidl's `uint8 structure_needs_at_least_one_member`, which a version-9
152/// tree names as the schema of every empty message and whose byte it writes.
153/// A version-8 tree still compiles and runs against this runtime: it names
154/// nothing that was withdrawn. What it keeps is the defect 1293 fixes for a
155/// regenerated tree, and only for ITS OWN empty messages: they still go out
156/// with no byte (which every stock RMW refuses) and still have no Cyclone
157/// descriptor (`BuildError::EmptySchema`, which says to regenerate). That is
158/// the behaviour the tree already had, not a new mismatch the runtime
159/// introduces, so the floor stays where it is.
160///
161/// RAISED to 10 when [`NROS_CODEGEN_VERSION`] moved to 10 (phase-483 W1), and
162/// this is the first move that needs it. The C++ user API left `nros::` for
163/// `rclcpp::`, and `nros::` was deleted rather than kept as an alias, so every
164/// name a version-9 C++ tree spells — `nros::FixedString<N>`,
165/// `nros::FixedSequence<T, N>`, `nros::HeapString`, `nros::HeapSequence<T>`,
166/// `nros::Span<T>` / `LeSpan<T>` / `StringView`, `nros::bind_subscription_sized`
167/// and `nros::detail::size_bound_dependent_false` — is WITHDRAWN. A version-9
168/// C++ tree does not compile against this runtime, so accepting it would
169/// replace the version mismatch this constant reports with a wall of
170/// unresolved-name errors. Rust and C trees are unaffected by the move, but the
171/// version is one number for all three, so they regenerate too.
172///
173/// Still 10 while [`NROS_CODEGEN_VERSION`] moved to 11 (phase-487 W1). The
174/// surface the gate saw move is `subscription.h`'s `static inline`
175/// `rclc_subscription_init_best_effort`, whose body forwards to
176/// `rclc_subscription_init_with_qos` and lost the two `NULL`s that function no
177/// longer takes. Generated C does not call either: a message header's typed
178/// registration is `rclc_executor_add_subscription_typed_sized`, whose
179/// signature is unchanged, and it passes `struct rcl_subscription_t *`, whose
180/// layout shrank but which generated code never dereferences. A version-10
181/// tree therefore still compiles and runs.
182///
183/// RAISED to 12 when [`NROS_CODEGEN_VERSION`] moved to 12 (phase-487 W2). A
184/// generated C service header's `<Srv>_service_init` called
185/// `rclc_service_init_with_qos` with the trampoline and handler in two
186/// positions that function no longer has, because the executor arena owns a
187/// dispatched server's callback and the struct keeps none. The binding moved
188/// to a new generated `<Srv>_executor_add_service` over
189/// `rclc_executor_add_service_raw`. A version-10 or -11 C tree with a service
190/// therefore does not compile against this runtime, so it is refused by
191/// version rather than by a wall of argument-count errors.
192///
193/// RAISED to 13 with [`NROS_CODEGEN_VERSION`] (phase-492 W3, RFC-0089
194/// amended 2026-10-12). The C API dropped `nros_`: every function, type and
195/// constant generated C names took its upstream spelling or `rclc_`
196/// (`docs/reference/c-api-rename-map.tsv`), with no forwarders. A version-12 C
197/// tree calls names this runtime no longer defines, so it is refused by
198/// version rather than by a wall of undeclared identifiers. Rust and C++ trees
199/// are unaffected, but the version is one number for all three.
200///
201/// RAISED to 14 with [`NROS_CODEGEN_VERSION`] (phase-492 W7/W8, RFC-0089
202/// "Messages take rosidl's C names AND its member shape" and
203/// "`ROSIDL_GET_*_TYPE_SUPPORT` adopted as a pass-through"). Generated C
204/// messages take rosidl's names (`std_msgs__msg__String` in
205/// `<std_msgs/msg/string.h>`) and member shape (`rosidl_runtime_c__String`,
206/// `__Sequence`, wired by a generated `__Storage` + `__init_in`); the type
207/// descriptors `rclc_message_type_t` / `rclc_service_type_t` /
208/// `rclc_action_type_t` gained codec slots and the generated
209/// `<pkg>__<sub>__<Name>__nros_type` fills them; the typed helpers
210/// (`<Msg>_publish`, `<Msg>_executor_add_subscription*`, `_subscribe*`) and
211/// `rclc_executor_add_subscription_typed{,_sized}` retired for `rcl_publish`
212/// and rclc's six-argument `rclc_executor_add_subscription_with_context`. A
213/// version-13 C tree names a descriptor layout and entry points this runtime
214/// no longer has, so it is refused by version. Rust and C++ trees are
215/// unaffected, but the version is one number for all three.
216///
217/// Still 14 while [`NROS_CODEGEN_VERSION`] moved to 15 (issue 1179, phase-498
218/// W3). Generated `RosMessage` impls now state `MAX_SERIALIZED_BOUND`, the
219/// type's size bound, so a typed subscription is sized from its type on every
220/// backend. The trait const is DEFAULTED (`None`), so a version-14 tree still
221/// compiles against this runtime and only keeps the image-wide receive size it
222/// had. Nothing it names is gone, so it is accepted.
223///
224/// Was 14 while [`NROS_CODEGEN_VERSION`] moved to 16 (issue 1913). Generated
225/// actions now carry the hash each of their five endpoints is keyed on: Rust
226/// impls state `RosAction::{CANCEL_GOAL_SERVICE,STATUS_MESSAGE}_HASH`, the C
227/// descriptor `rclc_action_type_t` gained a trailing `endpoint_hashes`, and C++
228/// action types gained five `<Endpoint>_HASH` members. All three are ADDITIVE
229/// on the runtime side: the Rust consts default to `ACTION_HASH`, an older C
230/// designated initializer leaves the trailing member NULL (read as "use
231/// `type_hash`"), and `rclcpp::detail::action_endpoint_hashes<A>()` detects the
232/// C++ members. So an older tree still compiles and runs with the single hash
233/// it always had, and is accepted; a NEW tree names a member an older runtime
234/// lacks, which is what the bump refuses.
235///
236/// 17 with [`NROS_CODEGEN_VERSION`] (phase-501 W1): the C API's headers moved
237/// from `nros/` into `rclc/` (`<rclc/types.h>`, `<rclc/detail/cdr.h>`,
238/// `<rclc/detail/view.h>`, ...), with no forwarding headers left behind. A
239/// version-16 C tree includes `<nros/types.h>`, which this runtime no longer
240/// ships, so it cannot compile and is refused by version instead. Rust and C++
241/// trees are unaffected, but the version is one number for all three.
242///
243/// 18 with [`NROS_CODEGEN_VERSION`] (phase-503 W1): the same move for C++. The
244/// nros-cpp headers left `nros/` for `rclcpp/`, `rclcpp/detail/`,
245/// `rclcpp_action/` and `rclcpp_lifecycle/`, again with no forwarding headers.
246/// A version-17 C++ tree includes `<nros/heap_string.hpp>`, which this runtime
247/// no longer ships, so it is refused by version rather than failing to compile.
248///
249/// The range `[NROS_CODEGEN_VERSION_MIN, NROS_CODEGEN_VERSION]` is expressed to
250/// C and C++ as a SET OF DEFINED SYMBOLS rather than as a comparison — see
251/// `nros-build-helpers`' codegen-version anchor — so there is no range check on
252/// that side that could itself be wrong.
253pub const NROS_CODEGEN_VERSION_MIN: u32 = 18;
254
255/// Does `emitted` fall in the range this runtime accepts?
256///
257/// The ONE comparison. Rust call sites reach it through
258/// `nros_node::codegen_version_check`, the CLI through `abi_guard`; neither
259/// re-spells the bounds.
260#[must_use]
261pub const fn accepts(emitted: u32) -> bool {
262 emitted >= NROS_CODEGEN_VERSION_MIN && emitted <= NROS_CODEGEN_VERSION
263}
264
265// The accepted range must be non-empty, checked at COMPILE time because it is a
266// property of two constants. A runtime test would be the wrong tool — and
267// clippy says so (`assertions_on_constants`) in every crate that `include!`s
268// this file, which is how it was found.
269const _: () = assert!(
270 NROS_CODEGEN_VERSION_MIN <= NROS_CODEGEN_VERSION,
271 "the accepted codegen range is empty: nothing could ever be compatible"
272);