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