Skip to main content

nros_core/
lib.rs

1//! Core types, traits, and abstractions for nros
2//!
3//! This crate provides the foundational types and traits for nros:
4//! - `RosMessage` trait for message types
5//! - `RosService` trait for service types
6//! - `RosAction` trait for action types
7//! - `ServiceServer` and `ServiceClient` for service communication
8//! - Time and Duration types
9//! - Error types
10
11#![no_std]
12
13// phase-359 W10 — no `std` feature; the test harness is hosted.
14#[cfg(test)]
15extern crate std;
16
17// phase-361 W2 (final) / issue 0598 — `alloc` is THE heap predicate, in this
18// exact spelling everywhere in the workspace. `std` reaches it through the
19// MANIFEST (`std = ["alloc", …]`), not through a wider `cfg`.
20//
21// A `std` build links an allocator by definition, so it does have a heap, and a
22// hosted consumer must not have to name `alloc` — but that implication belongs
23// in one manifest line per crate, not in every `cfg`. Spelling it
24// `any(alloc, std)` at the use sites was tried and reverted: it put the same
25// fact in 123 places and left phase-359 (which DELETES `std` from these crates)
26// with 88 extra branches to unwind, in `nros-node` above all. With the manifest
27// edge, dropping `std` needs no `cfg` edit at all — these gates are already in
28// their final form.
29//
30// Issue 0598 was never about which predicate: it was that THIS crate's heap
31// gate and `nros-serdes`'s disagreed, so a `std`-only build got a `heap::Vec<T>`
32// it could name and could not serialize. The fix is that `std` now forwards
33// `alloc` here, so the types and their impls arrive together.
34#[cfg(feature = "alloc")]
35extern crate alloc;
36
37pub mod action;
38pub mod clock;
39/// RFC-0090 / phase-429 — the codegen version, the one token that says whether
40/// generated code and this runtime can work together.
41///
42/// # What breaks without it
43///
44/// nano-ros shipped prebuilt `nros` binaries early and stopped, because a
45/// released binary emitted code that had drifted from the runtime. The failure
46/// is not loud: drifted generated code *compiles*, and the image is simply
47/// wrong in whatever way the generator was wrong. Issue 1018 records the
48/// canonical instance — the C emitter transposed sequence-of-strings dimensions
49/// in all three emission sites (`char data[256][64]` for `[64][256]`) — caught
50/// by a developer's build refusal, which is a thing no user has.
51///
52/// The relation that breaks is **generated code ↔ runtime**. The binary is only
53/// the thing that produced one side of it, which is why the version lives here,
54/// in the runtime, rather than in the CLI.
55///
56/// # Why an integer and not a hash
57///
58/// A hash cannot say WHICH SIDE IS BEHIND, and that distinction is the whole
59/// value of the token: `G < MIN` is "regenerate, silently" and `G > VERSION` is
60/// "a human must move a pin". They are not interchangeable remedies. A hash
61/// also moves when a doc comment moves, and a check that fires on cosmetic
62/// change is one people learn to bypass — `NROS_SKIP_VERSION_CHECK` is right
63/// there.
64///
65/// # Not to be confused with the codegen FINGERPRINT
66///
67/// `nros codegen-fingerprint` hashes every byte the emitters produce for a
68/// compiled-in corpus. It answers *"would this binary emit different bytes?"* —
69/// a FRESHNESS question, whose remedy is to regenerate, silently. The constants
70/// here answer *"can this code work with this runtime?"* — a COMPATIBILITY
71/// question, whose remedy is a refusal. Conflating them is why issue 1018's
72/// stale-CLI refusal both over-fires on `cmd/doctor.rs` and cannot ship.
73///
74/// The module's own file carries no `//!` docs deliberately; see the comment at
75/// its head.
76///
77/// # Where the negative case is proven
78///
79/// `just check codegen-version-refusal` builds artifacts carrying an
80/// out-of-range version and requires each language to refuse: `#error` for C and
81/// C++, `error[E0080]` for Rust. A check that has never been observed to fail is
82/// a check nobody has evidence still works, and this one guards a failure whose
83/// whole character is that it is silent.
84pub mod codegen_version;
85// issue 0783 — there is no `error` module here any more, and its absence is the
86// decision. It held ~~`NanoRosError { code: RclReturnCode, context, nested }`~~, a
87// phase-16 rclrs-shaped error, plus `RclReturnCode` (an `rcl_ret_t` numeric
88// mirror), ~~`ErrorContext`~~, ~~`NestedError`~~, ~~`NanoRosErrorFilter`~~ and
89// `TakeFailedAsNone`. Phase 84.D1 settled `NodeError` (nros-node) as the single
90// user-facing error and deferred "folding NanoRosError into NodeError"; the fold
91// never happened and nothing ever called the type. It was reachable from no
92// public API: the `nros` facade never re-exported it, and this crate's own
93// `RosAction::register_protocol_types` returns `Result<(), ()>` with a comment
94// saying it cannot name an error type — with ~~`NanoRosError`~~ sitting in the same
95// crate. RFC-0036's Errors row described it as the Rust user error for two
96// years, which is the cost this deletion removes.
97pub mod lifecycle;
98pub mod logger;
99pub mod message_info;
100pub mod service;
101pub mod service_info;
102pub mod time;
103pub mod types;
104
105// phase-417 W4.b — `ActionClient` / `ActionServer` left this list with the two
106// dead marker structs behind them; the live types under those names are
107// `nros_node`'s, which is what `nros` re-exports.
108pub use action::{
109    CancelResponse, CancelResponseCode, GoalInfo, GoalResponse, GoalStatusCode, GoalStatusStamped,
110    GoalUuid, RosAction,
111};
112#[allow(deprecated)]
113pub use action::{CancelReturnCode, GoalId, GoalStatus};
114pub use clock::{Clock, ClockType};
115// RFC-0090 — generated code names these directly, so they are re-exported at
116// the root: an emitted `const` assertion should not have to spell a module path
117// that could be reorganised under it.
118pub use codegen_version::{NROS_CODEGEN_VERSION, NROS_CODEGEN_VERSION_MIN};
119pub use lifecycle::{LifecycleState, LifecycleTransition, TransitionResult};
120pub use logger::{Logger, OnceFlag};
121pub use message_info::{MessageInfo, PUBLISHER_GID_SIZE, RawMessageInfo, pad_publisher_gid};
122pub use service::{ServiceCallback, ServiceClient, ServiceRequest, ServiceServer};
123pub use service_info::{REQUEST_WRITER_GUID_SIZE, RequestId, ServiceInfo};
124pub use time::{Duration, SecNanosecMsg, Time};
125pub use types::{RosMessage, RosService, ViewableMessage};
126
127// Re-export serdes types for convenience
128pub use nros_serdes::{
129    CdrReader, CdrWriter, DHeaderMark, DHeaderScope, DeserError, Deserialize, DeserializeView,
130    EncodingVersion, LeDecode, LeSliceView, SerError, Serialize,
131};
132
133// Re-export heapless for generated message types
134pub use heapless;
135
136/// Heap-backed containers for generated `mode = "heap"` message fields
137/// (RFC-0033). Gated on `alloc`, which `std` implies — so a hosted consumer
138/// gets these by asking for `std` and never has to name `alloc`. `nros-serdes`
139/// gates its matching `Serialize`/`Deserialize` impls on the SAME feature and
140/// receives it through the same forward, which is what issue 0598 was about.
141/// Generated code refers to `nros_core::heap::{Vec, String}` so the same path
142/// works in both crate and inline (`build.rs`) codegen modes.
143#[cfg(feature = "alloc")]
144pub mod heap {
145    pub use alloc::{string::String, vec::Vec};
146}
147
148/// RFC-0090 — tests for `codegen_version`, deliberately NOT inside that module's
149/// file. `nros-build-helpers` and `rosidl-codegen` `include!` it verbatim, so
150/// anything there is compiled into them too; the file is kept to constants and
151/// `const fn`s, and its behaviour is exercised from here.
152#[cfg(test)]
153mod codegen_version_tests {
154    use crate::codegen_version::*;
155
156    // The non-empty-range invariant is NOT a test here: it is a property of two
157    // constants, so it is a `const _: () = assert!(…)` in `codegen_version.rs`
158    // and is checked at compile time in every crate that includes that file.
159    // Written as a runtime test it is `clippy::assertions_on_constants`, which
160    // is a hard error under `-D warnings`.
161
162    #[test]
163    fn accepts_the_range_and_nothing_outside_it() {
164        assert!(accepts(NROS_CODEGEN_VERSION));
165        assert!(accepts(NROS_CODEGEN_VERSION_MIN));
166        assert!(
167            !accepts(NROS_CODEGEN_VERSION + 1),
168            "code emitted by a NEWER binary must be refused — the runtime is \
169             the older side and cannot know what changed"
170        );
171        assert!(
172            !accepts(NROS_CODEGEN_VERSION_MIN.saturating_sub(1)) || NROS_CODEGEN_VERSION_MIN == 0,
173            "code below the floor must be refused, not silently accepted"
174        );
175    }
176
177    /// Version 0 is reserved for "did not say".
178    ///
179    /// A generated artifact that carries no version reads as 0 through every
180    /// path that parses one, and must never be accepted: an artifact that did
181    /// not declare its version is exactly the pre-phase-429 artifact this
182    /// mechanism exists to catch.
183    #[test]
184    fn zero_is_never_accepted() {
185        assert!(!accepts(0));
186    }
187}