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
103pub use action::{
104    ActionClient, ActionServer, CancelResponse, CancelReturnCode, GoalId, GoalInfo, GoalResponse,
105    GoalStatus, GoalStatusStamped, RosAction,
106};
107pub use clock::{Clock, ClockType};
108// RFC-0090 — generated code names these directly, so they are re-exported at
109// the root: an emitted `const` assertion should not have to spell a module path
110// that could be reorganised under it.
111pub use codegen_version::{NROS_CODEGEN_VERSION, NROS_CODEGEN_VERSION_MIN};
112pub use lifecycle::{LifecycleState, LifecycleTransition, TransitionResult};
113pub use logger::{Logger, OnceFlag};
114pub use message_info::{MessageInfo, PUBLISHER_GID_SIZE, RawMessageInfo};
115pub use service::{ServiceCallback, ServiceClient, ServiceRequest, ServiceServer};
116pub use time::{Duration, Time};
117pub use types::{RosMessage, RosService, ViewableMessage};
118
119// Re-export serdes types for convenience
120pub use nros_serdes::{
121    CdrReader, CdrWriter, DHeaderMark, DHeaderScope, DeserError, Deserialize, DeserializeView,
122    EncodingVersion, LeDecode, LeSliceView, SerError, Serialize,
123};
124
125// Re-export heapless for generated message types
126pub use heapless;
127
128/// Heap-backed containers for generated `mode = "heap"` message fields
129/// (RFC-0033). Gated on `alloc`, which `std` implies — so a hosted consumer
130/// gets these by asking for `std` and never has to name `alloc`. `nros-serdes`
131/// gates its matching `Serialize`/`Deserialize` impls on the SAME feature and
132/// receives it through the same forward, which is what issue 0598 was about.
133/// Generated code refers to `nros_core::heap::{Vec, String}` so the same path
134/// works in both crate and inline (`build.rs`) codegen modes.
135#[cfg(feature = "alloc")]
136pub mod heap {
137    pub use alloc::{string::String, vec::Vec};
138}
139
140/// RFC-0090 — tests for `codegen_version`, deliberately NOT inside that module's
141/// file. `nros-build-helpers` and `rosidl-codegen` `include!` it verbatim, so
142/// anything there is compiled into them too; the file is kept to constants and
143/// `const fn`s, and its behaviour is exercised from here.
144#[cfg(test)]
145mod codegen_version_tests {
146    use crate::codegen_version::*;
147
148    // The non-empty-range invariant is NOT a test here: it is a property of two
149    // constants, so it is a `const _: () = assert!(…)` in `codegen_version.rs`
150    // and is checked at compile time in every crate that includes that file.
151    // Written as a runtime test it is `clippy::assertions_on_constants`, which
152    // is a hard error under `-D warnings`.
153
154    #[test]
155    fn accepts_the_range_and_nothing_outside_it() {
156        assert!(accepts(NROS_CODEGEN_VERSION));
157        assert!(accepts(NROS_CODEGEN_VERSION_MIN));
158        assert!(
159            !accepts(NROS_CODEGEN_VERSION + 1),
160            "code emitted by a NEWER binary must be refused — the runtime is \
161             the older side and cannot know what changed"
162        );
163        assert!(
164            !accepts(NROS_CODEGEN_VERSION_MIN.saturating_sub(1)) || NROS_CODEGEN_VERSION_MIN == 0,
165            "code below the floor must be refused, not silently accepted"
166        );
167    }
168
169    /// Version 0 is reserved for "did not say".
170    ///
171    /// A generated artifact that carries no version reads as 0 through every
172    /// path that parses one, and must never be accepted: an artifact that did
173    /// not declare its version is exactly the pre-phase-429 artifact this
174    /// mechanism exists to catch.
175    #[test]
176    fn zero_is_never_accepted() {
177        assert!(!accepts(0));
178    }
179}