nros/lib.rs
1//! # nros
2//!
3//! A lightweight ROS 2 client library for embedded systems.
4//!
5//! This crate provides a unified API for building ROS 2 nodes in Rust,
6//! with support for `no_std` environments and embedded targets.
7//!
8//! ## Features
9//!
10//! - **no_std compatible**: Works on bare-metal and RTOS targets
11//! - **Zero-copy where possible**: Minimizes memory allocations
12//! - **Type-safe**: Compile-time verification of message types
13//! - **ROS 2 compatible**: Interoperates with standard ROS 2 nodes via rmw_zenoh
14//!
15//! ## Quick Start
16//!
17//! ```ignore
18//! use nros::prelude::*;
19//! use std_msgs::msg::Int32;
20//!
21//! let config = ExecutorConfig::from_env().node_name("my_node");
22//! let mut executor = Executor::open(&config)?;
23//!
24//! let node = executor.node_builder("my_node").build()?;
25//! let publisher = executor.node_mut(node).create_publisher::<Int32>("/my_topic")?;
26//! publisher.publish(&Int32 { data: 42 })?;
27//!
28//! executor.node_mut(node).create_subscription::<Int32, _>("/topic", |msg: &Int32| {
29//! println!("Received: {}", msg.data);
30//! })?;
31//!
32//! executor.spin_blocking(SpinOptions::default());
33//! ```
34//!
35//! ## Executor Sizing
36//!
37//! The executor's static memory layout is controlled via environment variables
38//! at build time:
39//!
40//! - **`NROS_EXECUTOR_MAX_CBS`** (default 4) — maximum number of registered
41//! callbacks (subscriptions + timers + services + guard conditions).
42//! - **`NROS_EXECUTOR_ARENA_SIZE`** (default 4096) — byte budget for storing
43//! callback closures inline.
44//!
45//! For messages larger than the default 1024-byte receive buffer, size the
46//! subscription via the builder's `.rx_buffer::<N>()` knob (e.g.
47//! `node_mut(id).subscription(t).typed::<M>().rx_buffer::<4096>().build(cb)`).
48//!
49//! ## Transport Backends
50//!
51//! Phase 248 C5c — `nros` is RMW- and platform-AGNOSTIC. It carries only the
52//! `rmw-cffi` vtable; the concrete backend (zenoh / xrce / cyclonedds) enters
53//! the link graph via the board crate (embedded), the board-less app's own
54//! `nros-rmw-*` dep (native), or the `nros-c`/`nros-cpp` staticlib root (D3),
55//! and self-registers through the `RMW_INIT_ENTRIES` walker at `Executor::open`.
56//! The concrete session type is resolved automatically; advanced users can
57//! access it via `nros::internals::RmwSession`.
58//!
59//! ## Crate Features
60//!
61//! `nros` exposes only FUNCTIONAL features — `std`/`alloc`, the `rmw-cffi`
62//! vtable, `lending`, `bridge`/`config`, `param-services`,
63//! `lifecycle-services`, `safety-e2e`, `stream`, `ffi-sync`, and the ROS
64//! edition (`ros-humble`/`ros-iron`). There are NO `platform-*` or concrete
65//! `rmw-*` selector features (Phase 248 C7). Platform + RMW are selected by the
66//! board / staticlib root via dependencies, not `nros` features. The
67//! `zephyr_component_main!` entry macro is gated only on `rmw-cffi` (it's
68//! framework entry codegen, like `nros::main!`), not a platform feature.
69//!
70//! **ROS edition** (select one; RFC-0056 — the per-distro interop profile):
71//! - `ros-humble` - ROS 2 Humble (default; `TypeHashNotSupported`, XCDR1)
72//! - `ros-iron` - ROS 2 Iron (RIHS01 type hash)
73//! - `ros-jazzy` - ROS 2 Jazzy (RIHS01; XCDR2/appendable is phase-303)
74//!
75//! **Other**:
76//! - `std` (default) - Enable standard library support
77//! - `alloc` - Enable heap allocation without full std
78//!
79//! ## Further Reading
80//!
81//! - [`guide`] — tutorials: getting started, services, configuration,
82//! ROS 2 interop, and troubleshooting
83//! - [Message Generation](https://github.com/jerry73204/nano-ros/blob/main/docs/guides/message-generation.md)
84//! — codegen reference (all options, output structure, bundled interfaces)
85//! - [Environment Variables](https://github.com/jerry73204/nano-ros/blob/main/docs/reference/environment-variables.md)
86//! — complete buffer tuning reference
87//! - [ROS 2 Interop](https://github.com/jerry73204/nano-ros/blob/main/docs/reference/rmw_zenoh_interop.md)
88//! — protocol details (key expressions, liveliness, attachments)
89//! - [Examples](https://github.com/jerry73204/nano-ros/tree/main/examples)
90//! — working examples by platform (native, QEMU, ESP32, Zephyr)
91
92#![no_std]
93
94// ── Feature validation (mutual exclusivity) ─────────────────────────────
95// Phase 248 C5c/C7 — `nros` carries NO `platform-*` selector features, so the
96// platform mutual-exclusion `compile_error!` is gone. The platform is selected
97// by the board / staticlib root via an `nros-platform` dep, and nros-node picks
98// the kernel primitive at runtime (C2 wake-probe).
99// Only `rmw-cffi` is exposed at this layer; the cffi shim selects the
100// concrete backend at the C ABI level via the `RMW_INIT_ENTRIES` walker.
101
102// At most one ROS edition (RFC-0056 — the axis is compile-time exclusive).
103#[cfg(any(
104 all(
105 feature = "ros-humble",
106 any(feature = "ros-iron", feature = "ros-jazzy")
107 ),
108 all(feature = "ros-iron", feature = "ros-jazzy"),
109))]
110compile_error!("`ros-{humble,iron,jazzy}` are mutually exclusive — select one ROS edition.");
111
112#[cfg(feature = "std")]
113extern crate std;
114
115#[cfg(feature = "alloc")]
116extern crate alloc;
117
118// Phase 216.A.5 — the `nros::node!()` proc-macro emits absolute paths
119// under `::nros::*` (so downstream Node pkgs only need a single `nros`
120// dep). For the in-crate macro-expansion test in `node.rs`, alias the
121// `nros` crate name to itself so those absolute paths resolve. Gated on
122// `cfg(test)` to keep the alias out of normal builds.
123#[cfg(test)]
124extern crate self as nros;
125
126// Phase 248 C5c — the umbrella's force-link statics
127// (`__FORCE_LINK_{PLATFORM_CFFI,ZENOH,XRCE,CYCLONEDDS_SYS}`) are REMOVED along
128// with `nros`'s concrete-backend deps. `nros` no longer references any concrete
129// RMW or platform crate, so it has nothing to force-link. Registration + the
130// `nros_platform_*` link anchor now live with whoever owns the concrete crate:
131// * embedded — the BOARD crate force-links its backend + calls
132// `<backend>::register()` in its boot path (C5a);
133// * board-less native — the APP owns `nros-rmw-*` + a `#[used]` force-link in
134// its `main.rs`, and `nros-platform-cffi[posix-c-port]` anchors the C symbols;
135// * C/C++ staticlib — `nros-c`/`nros-cpp` bundle one backend (D3) and anchor
136// `nros-platform` themselves.
137
138// Phase 249 P1 — `__register_linked_rmw()` (a Phase 248 C5c no-op kept only so the
139// `nros::main!` framework's call sites compiled) is REMOVED along with those call
140// sites. Backend registration never routed through the backend-agnostic `nros` crate:
141// hosted auto-registers via the `RMW_INIT_ENTRIES` walk at `Executor::open`; embedded
142// boards perform the explicit `<backend>::register()` in their boot path (C5a). One
143// Rust trigger = the board/app explicit register (phase-249).
144
145// phase-391 W5 — build-time knobs (`MAX_COMPONENTS`, `COMPONENT_SLOT_BYTES`).
146// Ungated: they are plain consts, useful to size caller-supplied storage
147// whether or not the runtime module that consumes them is compiled in.
148pub mod config;
149
150// phase-391 W5 — caller-supplied component-pool storage sizing. Ungated for the
151// same reason `config` is: plain arithmetic, useful to size a `static` whether
152// or not `node_runtime` is compiled in.
153pub mod dispatch_tag;
154pub mod guide;
155#[cfg(feature = "metadata-mode")]
156pub mod metadata_mode;
157pub mod node;
158pub mod node_metadata;
159/// Phase 212.M.5.a.2 — executor-backed component runtime.
160///
161/// Binds [`Node`] / [`ExecutableNode`] to a live
162/// [`Executor`] so a Node pkg can actually run (versus
163/// [`MetadataRecorder`](node_metadata::MetadataRecorder) which
164/// is the planner-side metadata sink).
165///
166/// Gated on `rmw-cffi`; the underlying [`Executor`] is only present
167/// when an RMW backend is linked. W5-endgame (issue 0843): the MACRO install
168/// path (per-class static storage, placed cells, slabbed ctxs) is alloc-free,
169/// so the module no longer demands `alloc` — only the dynamic
170/// `ExecutorNodeRuntime` half does, item-gated inside.
171#[cfg(feature = "rmw-cffi")]
172pub mod node_runtime;
173pub mod runtime_storage;
174
175/// Phase 212.L.5 — top-level init API.
176///
177/// Re-exported flat at the crate root: `nros::init()`,
178/// `nros::init_with_launch_auto()`, `nros::init_with_launch(path)`,
179/// `nros::init_with_args(args)`, `nros::Context`, `nros::InitError`.
180#[cfg(feature = "env")]
181pub mod init;
182
183/// issue 0687 — the hosted edge of configuration: every env var nano-ros
184/// honours is read here, and the core takes values.
185#[cfg(feature = "env")]
186pub mod env;
187
188#[cfg(feature = "env")]
189pub use env::{ExecutorConfigEnvExt, rmw_selector};
190
191#[cfg(feature = "env")]
192pub use init::{
193 Context, ContextSource, InitError, init, init_with_args, init_with_launch,
194 init_with_launch_auto,
195};
196
197/// Compile-time opaque storage sizes for FFI consumers.
198///
199/// See [`sizes`] for the `export_size!` pattern used to expose these values
200/// to `nros-c` / `nros-cpp` at build time.
201pub mod sizes;
202
203/// Monotonic time for portable node code (issue #504).
204///
205/// phase-359 W10 — `rmw-cffi`, not `any(std, rmw-cffi)`: the clock is the
206/// platform port's, and a build with no port has none to offer.
207#[cfg(feature = "rmw-cffi")]
208pub mod time;
209
210/// CDR encapsulation constants and helpers for FFI layers that handle raw
211/// CDR bytes (e.g. nros-c, nros-cpp action and service paths).
212pub mod cdr {
213 pub use nros_serdes::{
214 CDR_BE_HEADER, CDR_HEADER_LEN, CDR_LE_HEADER, strip_cdr_header, write_cdr_le_header,
215 };
216}
217
218// Re-export core types
219pub use nros_core::{
220 CdrReader, CdrWriter, Clock, ClockType, DeserError, Deserialize, Duration, Logger, MessageInfo,
221 PUBLISHER_GID_SIZE, RawMessageInfo, RosMessage, RosService, SerError, Serialize, Time,
222};
223
224// Re-export heapless for generated message types and examples
225pub use nros_core::heapless;
226
227// Re-export component-mode API
228#[cfg(feature = "rmw-cffi")]
229pub use node::NodeExecutorRuntime;
230// Phase 212.M.5.a.2 — executor-backed runtime entry points.
231// (`component_register_symbol` retired in the Phase 212.N.7 closing
232// sweep — the helper had no live callers after the BSP baker + macro
233// extern emit were deleted.)
234pub use node::{
235 ActionExecutor, Callback, CallbackCtx, CallbackEffects, ClientDispatch, DeclaredNode,
236 DeclaredNodeRuntime, EntityBounds, ExecutableNode, MISSING_NODE_EXPORT_ERROR, Node,
237 NodeActionClient, NodeActionServer, NodeContext, NodeDeclError, NodeOptions, NodeParameter,
238 NodePublisher, NodeResult, NodeRuntime, NodeRuntimeAdapter, NodeServiceClient,
239 NodeServiceServer, NodeSubscription, NodeTimer, PublisherResolver, RuntimeNodeRecord, TickCtx,
240 record_node_metadata, register_node,
241};
242// Phase 212.M.5.a.4 — internal helper consumed by `nros::node!()`
243// for the BSP dispatch path. Public-but-doc-hidden so the macro expand
244// resolves it as `::nros::__private_node_state_into_raw`.
245#[cfg(feature = "alloc")]
246#[doc(hidden)]
247pub use node::__private_node_state_into_raw;
248// phase-359 W8 — follows `node_metadata`'s re-gate: the type needs `alloc`,
249// not `std`.
250#[cfg(feature = "alloc")]
251pub use node_metadata::SourceMetadataExport;
252pub use node_metadata::{
253 CallbackEffectKind, CallbackEffectMetadata, EntityKind, EntityMetadata, MetadataRecorder,
254 MetadataString, NodeMetadata, NodeMetadataError, ParameterDefault, SourceLocationMetadata,
255 SourceNameKind,
256};
257#[doc(hidden)]
258pub use node_metadata::{CallbackId, EntityId, NodeId};
259// Phase 216.A.4 — opaque tag types Node authors hold on `Self::State`
260// and match against the `Callback<'_>` delivered to
261// `ExecutableNode::on_callback`.
262pub use dispatch_tag::{ActionTag, ServiceTag, SubscriptionTag};
263// W5-endgame (issue 0843) — the alloc-free half of the seam: per-class static
264// storage + the `_in` installs the macro emits. Available on `rmw-cffi` alone.
265#[cfg(feature = "rmw-cffi")]
266pub use node_runtime::{
267 ComponentSlotStorage,
268 // Phase 257 (W0-B) — the uniform cross-language component-install seam backing
269 // `__nros_component_<pkg>_install` (nros::node!): register an ExecutableNode on the
270 // shared executor a foreign typed entry hands in. (`register_node_borrowed` stays
271 // crate-internal — it returns the private `ComponentCell`.)
272 install_node_typed_in,
273 // Phase 305 W3 (issue 0255) — same seam plus launch `<remap>` rules; the variant
274 // `nros::node!()` emits.
275 install_node_typed_with_launch_in,
276};
277// The dynamic runtime + the leak-per-call conveniences still need `alloc`.
278#[cfg(all(feature = "rmw-cffi", feature = "alloc"))]
279pub use node_runtime::{
280 ExecutorError,
281 ExecutorNodeRuntime,
282 RegisteredNode,
283 install_node_typed,
284 install_node_typed_with_launch,
285 // Phase 268 W1 — same seam with both `<param>` initials AND `<node name= namespace=>`
286 // identity injection (RFC-0046).
287 install_node_typed_with_node_identity,
288 // W4a — same seam, seeding the node's NodeContext with launch-baked `<param>` initials.
289 install_node_typed_with_params,
290};
291
292/// Phase 257 (W0-B) — `install_node_typed` stub for builds without the cffi runtime.
293/// The typed-entry install seam needs the `rmw-cffi` executor; a `nros::node!()` pkg
294/// compiled without `rmw-cffi` still emits `__nros_component_<pkg>_install` (the macro
295/// can't see the umbrella's feature), so this stub keeps it linkable — it returns `-1`
296/// (no real executor to install on). The real impl is `node_runtime::install_node_typed`.
297///
298/// # Safety
299/// Signature parity with the real impl; the stub dereferences nothing.
300#[cfg(not(feature = "rmw-cffi"))]
301#[doc(hidden)]
302pub unsafe fn install_node_typed<C: node::ExecutableNode + 'static>(
303 _executor: *mut core::ffi::c_void,
304) -> i32
305where
306 C::State: 'static,
307{
308 -1
309}
310
311/// W4a — `install_node_typed_with_params` stub for builds without the cffi runtime.
312/// Signature parity with `node_runtime::install_node_typed_with_params`; returns `-1`.
313///
314/// # Safety
315/// The stub dereferences nothing.
316#[cfg(not(feature = "rmw-cffi"))]
317#[doc(hidden)]
318pub unsafe fn install_node_typed_with_params<C: node::ExecutableNode + 'static>(
319 _executor: *mut core::ffi::c_void,
320 _params: &[(&str, &str)],
321) -> i32
322where
323 C::State: 'static,
324{
325 -1
326}
327
328/// Phase 268 W1 — `install_node_typed_with_node_identity` stub for builds without the
329/// cffi runtime. Signature parity with the real impl; returns `-1`.
330///
331/// # Safety
332/// The stub dereferences nothing.
333#[cfg(not(feature = "rmw-cffi"))]
334#[doc(hidden)]
335pub unsafe fn install_node_typed_with_node_identity<C: node::ExecutableNode + 'static>(
336 _executor: *mut core::ffi::c_void,
337 _params: &[(&str, &str)],
338 _node_identity: Option<(&'static str, &'static str)>,
339) -> i32
340where
341 C::State: 'static,
342{
343 -1
344}
345
346/// phase-391 W5.3b — `ComponentSlotStorage` stub for builds without the cffi
347/// runtime (or without `alloc`): the macro emits a per-class
348/// `static ... = ComponentSlotStorage::new()` unconditionally, so the name must
349/// exist and be const-constructible + `Sync` in every cfg. Zero-sized.
350#[cfg(not(feature = "rmw-cffi"))]
351#[doc(hidden)]
352pub struct ComponentSlotStorage<
353 C,
354 const N: usize = { crate::config::MAX_CLASS_INSTANCES },
355 const PUBS: usize = { crate::config::MAX_CELL_ENTITIES },
356 const SVCS: usize = { crate::config::MAX_CELL_ENTITIES },
357 const ACTC: usize = { crate::config::MAX_CELL_ENTITIES },
358 const ACTS: usize = { crate::config::MAX_CELL_ENTITIES },
359 const SSRV: usize = { crate::config::MAX_CELL_ENTITIES },
360> {
361 _p: core::marker::PhantomData<fn() -> C>,
362}
363
364#[cfg(not(feature = "rmw-cffi"))]
365impl<C, const N: usize, const PUBS: usize, const SVCS: usize, const ACTC: usize, const ACTS: usize>
366 ComponentSlotStorage<C, N, PUBS, SVCS, ACTC, ACTS>
367{
368 #[doc(hidden)]
369 #[allow(clippy::new_without_default)]
370 pub const fn new() -> Self {
371 Self {
372 _p: core::marker::PhantomData,
373 }
374 }
375}
376
377/// phase-391 W5.3b — `install_node_typed_in` stub; returns `-1`.
378///
379/// # Safety
380/// Signature parity with the real impl; the stub dereferences nothing.
381#[cfg(not(feature = "rmw-cffi"))]
382#[doc(hidden)]
383pub unsafe fn install_node_typed_in<
384 C: node::ExecutableNode + 'static,
385 const N: usize,
386 const PUBS: usize,
387 const SVCS: usize,
388 const ACTC: usize,
389 const ACTS: usize,
390 const SSRV: usize,
391>(
392 _executor: *mut core::ffi::c_void,
393 _store: &'static ComponentSlotStorage<C, N, PUBS, SVCS, ACTC, ACTS, SSRV>,
394) -> i32
395where
396 C::State: 'static,
397{
398 -1
399}
400
401/// phase-391 W5.3b — `install_node_typed_with_launch_in` stub; returns `-1`.
402///
403/// # Safety
404/// Signature parity with the real impl; the stub dereferences nothing.
405#[cfg(not(feature = "rmw-cffi"))]
406#[doc(hidden)]
407pub unsafe fn install_node_typed_with_launch_in<
408 C: node::ExecutableNode + 'static,
409 const N: usize,
410 const PUBS: usize,
411 const SVCS: usize,
412 const ACTC: usize,
413 const ACTS: usize,
414 const SSRV: usize,
415>(
416 _executor: *mut core::ffi::c_void,
417 _store: &'static ComponentSlotStorage<C, N, PUBS, SVCS, ACTC, ACTS, SSRV>,
418 _params: &[(&str, &str)],
419 _node_identity: Option<(&'static str, &'static str)>,
420 _remaps: &[(&str, &str)],
421 // Spelled as the plain tuple rather than `QoSOverrideCode`: that alias
422 // lives behind nros-node's cffi gate and is unnameable in this cfg, but
423 // its definition is this tuple — the same spelling `RuntimeCtx` uses, so
424 // emitted calls typecheck identically against stub and real. (The OLDER
425 // with_launch stub above simply dropped this parameter, which is 4-vs-5
426 // signature drift against its real impl — not repeated here.)
427 _qos_overrides: &'static [(&'static str, u8, u8, u32)],
428) -> i32
429where
430 C::State: 'static,
431{
432 -1
433}
434
435/// Phase 305 W3 (issue 0255) — `install_node_typed_with_launch` stub for builds
436/// without the cffi runtime. Signature parity with the real impl; returns `-1`.
437///
438/// # Safety
439/// The stub dereferences nothing.
440#[cfg(not(feature = "rmw-cffi"))]
441#[doc(hidden)]
442pub unsafe fn install_node_typed_with_launch<C: node::ExecutableNode + 'static>(
443 _executor: *mut core::ffi::c_void,
444 _params: &[(&str, &str)],
445 _node_identity: Option<(&'static str, &'static str)>,
446 _remaps: &[(&str, &str)],
447) -> i32
448where
449 C::State: 'static,
450{
451 -1
452}
453// Phase 212.N.12 — canonical `nros::node!()` macro. Replaces the legacy
454// `nros::node!()` macro (retired in the N.12 hard rename — both the
455// proc-macro forwarder and the Cargo metadata key are gone).
456#[cfg(feature = "macros")]
457pub use nros_macros::node;
458// Phase 212.N.9 — `nros::main!()` proc-macro family. One-line Entry-pkg
459// `main.rs` (replaces the legacy `build.rs + include!()` shape). See
460// `docs/design/0024-multi-node-workspace-layout.md` §11.6.
461#[cfg(feature = "macros")]
462pub use nros_macros::main;
463
464/// Route this image's panics to its platform — phase-366 W5.c / RFC-0077.
465///
466/// Emits the `#[panic_handler]` for an embedded image, forwarding the message
467/// to `nros_platform_panic` so a Rust panic ends the same way a C precondition
468/// failure or a C++ terminate does. What that ending IS belongs to the port:
469/// `k_panic()` on Zephyr, `esp_system_abort()` on ESP-IDF, UART-then-exit-QEMU
470/// on the ThreadX RV64 board.
471///
472/// # Why this is a macro you INVOKE, not something `nros::main!` emits
473///
474/// `#[panic_handler]` is a singleton of the final artifact, and the image owns
475/// it. Emitting one silently from `nros::main!()` would collide with every image
476/// that already declares its own — `examples/qemu-esp32-baremetal` writes
477/// `use esp_backtrace as _;`, and `logging-smoke-freertos-mps2` uses
478/// `panic-semihosting` with `features = ["exit"]` so a panic exits QEMU instead
479/// of hanging the test harness. Those images are RIGHT, and an invisible default
480/// would fight them.
481///
482/// So the line is written in the entry, where it can be read, swapped for
483/// `use panic_halt as _;`, or replaced by a hand-written handler that logs to
484/// NVM and reboots. A default you cannot see is a constraint, not a default.
485///
486/// # Use — `main!(panic = …)` is the normal way; this is the escape hatch
487///
488/// phase-366 R3. An entry that goes through `nros::main!()` should say
489/// `panic = "platform"` (or nothing — that is the default since M5) and let the
490/// macro emit this body. One line, and the build can check it.
491///
492/// ```ignore
493/// #![no_std]
494/// nros::main!(); // ends through `nros_platform_panic`
495/// ```
496///
497/// Invoke this macro directly only where no `main!()` expansion can carry the
498/// item — a hand-rolled `no_std` binary, or the lib side of a crate whose
499/// `crate-type` includes `staticlib` and whose entry macro is
500/// `zephyr_component_main!` / a board `app_main!`. It is kept for exactly those:
501/// deleting it would strand the images that cannot use the macro replacing it.
502///
503/// ```ignore
504/// // src/app_main.rs — the .a is a final artifact to rustc, and `main!()` in
505/// // the bin target never reaches it.
506/// nros::panic_to_platform!();
507/// ```
508///
509/// Do NOT invoke it in a `std` image: libstd supplies the lang item there and a
510/// second one does not compile. Do not invoke it alongside `use panic_halt as _`
511/// or any other provider, for the same reason — that is the duplicate
512/// `check-archive-lang-items` exists to catch.
513/// Park the core on panic — the `halt` value of `nros::main!(panic = …)`.
514///
515/// For an image that must not print: no formatting, no allocation, no call out
516/// to the platform. Interrupts are masked first so the parked core cannot be
517/// woken back into a half-dead system by a timer or a driver ISR still armed
518/// from before the panic.
519///
520/// This is a body rather than a re-export of the `panic-halt` crate so that
521/// choosing it costs the entry no new dependency — `main!(panic = "halt")` is a
522/// word in a macro the crate already calls, which is the point of the surface.
523/// The behaviour is the same: mask, then spin forever.
524///
525/// Prefer `panic = "platform"`. Halting discards the diagnosis, and every port
526/// implements `nros_platform_panic` precisely so a dying image can say why.
527/// Phase 392 W3b — the receive-buffer size for a message type, as a constant
528/// that cannot drift from the type.
529///
530/// ```ignore
531/// node.subscription::<PointCloud2>("points")
532/// .rx_buffer::<{ nros::rx_buffer_for!(PointCloud2) }>()
533/// .build(on_cloud)?;
534/// ```
535///
536/// `.rx_buffer::<N>()` has always accepted a number. The problem is where the
537/// number comes from: a literal is correct until someone appends a field to the
538/// message, and then it is silently too small — the sample is received, ACKed
539/// and dropped at the transport, which needs a packet capture to attribute
540/// (`report_dropped_take`, and the 13.4 KiB Autoware trajectory case). This
541/// expands to the type's own bound, computed from its schema by phase 380, so
542/// appending a field moves the buffer with it.
543///
544/// **Why a macro and not a method.** The builder cannot do this for you:
545/// inside `impl<M> TypedSubscriptionBuilder<M>` the type is a generic
546/// parameter, and on stable Rust a generic parameter may not appear in a const
547/// operation — `error: generic parameters may not be used in const operations`.
548/// At a call site the type is CONCRETE, which is legal, so the size has to be
549/// named where the type is. That constraint is also why phase-392's size
550/// classes were "decoupled from codegen" in the first place.
551///
552/// **Unbounded types are a BUILD ERROR** (phase-403 W0). A type with an
553/// unbounded `string`/`wstring`/`sequence` has no bound, and this refuses to
554/// invent one. It used to expand to `DEFAULT_RX_BUF_SIZE`; it now fails to
555/// compile, naming both remedies.
556///
557/// Every message type is REQUIRED to carry a derived upper bound, stated in the
558/// `.msg` (`string<=64`) or capped in `nros-codegen.toml`. Phase 380 is explicit
559/// that `None` means "no bound EXISTS", never "unknown", and that a buffer must
560/// not be sized from a fallback. Substituting the configured default was the
561/// violation of that rule; refusing is what it licenses. `report_dropped_take`
562/// is a backstop for a buffer that is too small, not a licence to pick one.
563///
564/// ```compile_fail
565/// use nros_serdes::schema::{Field, FieldType, Message};
566/// struct Unbounded;
567/// impl Message for Unbounded {
568/// const TYPE_NAME: &'static str = "test/msg/Unbounded";
569/// const FIELDS: &'static [Field] = &[Field {
570/// name: "s",
571/// ty: FieldType::String,
572/// offset: 0,
573/// }];
574/// }
575/// let _n: usize = nros::rx_buffer_for!(Unbounded);
576/// ```
577///
578/// The positive control for that `compile_fail`, so it cannot pass because the
579/// fixture stopped compiling for an unrelated reason:
580///
581/// ```
582/// use nros_serdes::schema::{Field, FieldType, Message};
583/// struct Bounded;
584/// impl Message for Bounded {
585/// const TYPE_NAME: &'static str = "test/msg/Bounded";
586/// const FIELDS: &'static [Field] = &[Field {
587/// name: "a",
588/// ty: FieldType::Uint64,
589/// offset: 0,
590/// }];
591/// }
592/// let n: usize = nros::rx_buffer_for!(Bounded);
593/// assert!(n > 0);
594/// ```
595///
596/// **Why the `const` block.** The macro is used in two positions: as a
597/// const-generic argument (`.rx_buffer::<{ rx_buffer_for!(M) }>()`) and as a
598/// plain expression (`let n = rx_buffer_for!(M);`). A bare `panic!` is a build
599/// error in the first and a RUNTIME panic in the second, which would make the
600/// rule depend on where the macro appears. Wrapping the whole match in an inline
601/// `const` block forces compile-time evaluation in BOTH, so an unbounded type
602/// can never reach a running image.
603///
604/// **What the error names.** rustc points at this macro invocation, where the
605/// type is written literally, so the TYPE is named. The MEMBER that costs the
606/// bound cannot be in the message: const evaluation does not format, so a
607/// `panic!` there takes a literal only. The member is named by the codegen
608/// diagnostic for the same type -- `unbounded_reason` in the generated C header
609/// (`packs/c/message.h.jinja`), which names EVERY member that costs the bound in
610/// one build (phase-403 W0), or `nros_serdes::size::visit_unbounded` over its
611/// `FIELDS`.
612#[macro_export]
613macro_rules! rx_buffer_for {
614 ($msg:ty) => {
615 // The `const` block is load-bearing; see "Why the `const` block" above.
616 const {
617 match $crate::__rx_bound::<$msg>() {
618 ::core::option::Option::Some(n) => n,
619 ::core::option::Option::None => ::core::panic!(
620 "nros: this message type has NO serialized-size bound, so no \
621 receive buffer can be sized from it.\n\
622 Every message type must carry a derived upper bound. Bound the \
623 member that costs it, either:\n\
624 \x20 - in the `.msg`: `string<=64`, `wstring<=64`, \
625 `sequence<T, N>`, `T[<=N]`; or\n\
626 \x20 - as an INLINE `cap` in `nros-codegen.toml`: under \
627 `[fields]`, `\"pkg/Msg.field\" = 64`. A `heap` or `view` cap \
628 is a sizing hint that nothing enforces (RFC-0033), so it \
629 deliberately does NOT bound.\n\
630 WHICH members cost the bound -- all of them, not just the \
631 first -- are named by the codegen diagnostic for this same \
632 type: `unbounded_reason` in the generated C header, or \
633 `nros_serdes::size::visit_unbounded` over its `FIELDS`. The type itself is named by the \
634 `rx_buffer_for!` invocation rustc points at.\n\
635 Phase 380: `None` means no bound EXISTS, never \"unknown\", and \
636 a buffer sized from a fallback is the failure that rule was \
637 written to prevent. Erroring honours it; defaulting to \
638 `DEFAULT_RX_BUF_SIZE` did not."
639 ),
640 }
641 }
642 };
643}
644
645#[macro_export]
646macro_rules! panic_halt {
647 () => {
648 #[panic_handler]
649 fn __nros_panic_halt(_info: &::core::panic::PanicInfo) -> ! {
650 // Mask interrupts through the platform's critical section, which is
651 // the one IRQ primitive that is portable across the ports (the
652 // `cortex_m`/`riscv` intrinsics are not). Entering and never
653 // leaving is deliberate.
654 unsafe extern "C" {
655 fn nros_platform_critical_section_acquire() -> u32;
656 }
657 // SAFETY: the ABI's acquire takes no argument and returns a restore
658 // token we deliberately drop — nothing after this point runs.
659 unsafe {
660 let _ = nros_platform_critical_section_acquire();
661 }
662 loop {
663 ::core::hint::spin_loop();
664 }
665 }
666 };
667}
668
669#[macro_export]
670macro_rules! panic_to_platform {
671 () => {
672 #[panic_handler]
673 fn __nros_panic(info: &::core::panic::PanicInfo) -> ! {
674 use ::core::fmt::Write as _;
675
676 // Fixed buffer, never the heap: this runs when the allocator may be
677 // exactly what failed. Truncation is deliberate — a short panic line
678 // still diagnoses; a missing one does not.
679 struct Buf {
680 bytes: [u8; 192],
681 used: usize,
682 }
683 impl ::core::fmt::Write for Buf {
684 fn write_str(&mut self, s: &str) -> ::core::fmt::Result {
685 let room = self.bytes.len() - self.used;
686 let n = s.len().min(room);
687 self.bytes[self.used..self.used + n].copy_from_slice(&s.as_bytes()[..n]);
688 self.used += n;
689 Ok(())
690 }
691 }
692
693 let mut buf = Buf {
694 bytes: [0u8; 192],
695 used: 0,
696 };
697 let _ = write!(buf, "{info}");
698
699 unsafe extern "C" {
700 fn nros_platform_panic(msg: *const u8, len: usize) -> !;
701 }
702 // SAFETY: `buf.bytes[..used]` is initialised and outlives the
703 // diverging call; the ABI takes a length-delimited diagnostic, not
704 // a C string.
705 unsafe { nros_platform_panic(buf.bytes.as_ptr(), buf.used) }
706 }
707 };
708}
709
710/// Define Zephyr's `rust_main` for a self-bringup Rust component package.
711///
712/// The macro is intended for `rust_cargo_application()` apps whose crate
713/// already invokes `nros::node!()`. It opens a Zephyr executor, registers
714/// the supplied component through [`ExecutorNodeRuntime`], and spins forever.
715/// Issue 0330 — force-link an RMW backend crate into a pure-Rust staticlib.
716///
717/// On a Rust-only image (Zephyr and friends) the Zephyr module emits a weak
718/// `nros_rmw_<name>_register` and calls it only if it resolves. The strong
719/// definition is the backend crate's `#[no_mangle]` export — and rustc's
720/// staticlib DCE drops it unless something in the crate being compiled into the
721/// staticlib references that crate. The symbol is then present in the rlib and
722/// absent from the `.a`, the weak call sees NULL, and the image comes up with no
723/// backend registered (issues 0155 / 0163).
724///
725/// This emits the reference, without naming any backend in nano-ros' own
726/// RMW-agnostic layers — the app crate names it, because the app crate is what
727/// selects an RMW:
728///
729/// ```ignore
730/// #[cfg(feature = "rmw-zenoh")]
731/// nros::force_link_backend!(nros_rmw_zenoh);
732/// #[cfg(feature = "rmw-xrce")]
733/// nros::force_link_backend!(nros_rmw_xrce_cffi);
734/// ```
735///
736/// It is an ANCHOR, not a registration call — the static is never executed
737/// (same class as `nros-c`'s `FORCE_LINK` and `nros-rmw-cffi`'s section anchor).
738/// Registration happens through `nros_app_register_backends`. Backends whose
739/// register entry lives in a C/C++ library the image already links (cyclonedds
740/// on Zephyr) need no anchor at all.
741///
742/// Invoke at module scope. Multiple invocations in one crate are fine — each
743/// expands inside its own anonymous const, so the static names cannot collide.
744#[macro_export]
745macro_rules! force_link_backend {
746 // `ident`, not `path`: a `path` fragment may not be followed by `::`, so
747 // `$backend::register()` fails to parse at the CALL site with a misleading
748 // "expected an operator". Backend crate names are single idents anyway.
749 ($backend:ident) => {
750 const _: () = {
751 #[used]
752 static __NROS_FORCE_LINK_BACKEND: fn() = || {
753 let _ = $backend::register();
754 };
755 };
756 };
757}
758
759// Phase 248 C7 (Method A) — gated on `rmw-cffi` only (needs `Executor`), NOT a
760// `platform-*` feature. This is a framework ENTRY macro (same category as
761// `nros::main!`'s zephyr `rust_main` codegen) — `#[macro_export]` so it emits
762// nothing unless a Zephyr example invokes it; the body's `::zephyr::*` /
763// `::nros_platform::zephyr::wait_network` resolve only in that zephyr-build
764// context (the example deps the `zephyr` crate + `nros-platform[platform-zephyr]`).
765#[cfg(feature = "rmw-cffi")]
766#[macro_export]
767macro_rules! zephyr_component_main {
768 ($node:ty) => {
769 #[unsafe(no_mangle)]
770 pub extern "C" fn rust_main() {
771 unsafe {
772 zephyr::set_logger().ok();
773 }
774 // Phase 248 C7 step 1 — relocated helper (was `$crate::platform::zephyr`).
775 let _ = ::nros_platform::zephyr::wait_network(2000);
776 // Phase 249 P1 — RMW register is board/platform-owned (Phase 248 C5a);
777 // the backend-agnostic `nros` crate cannot register (no backend dep).
778 // Issue 0155 — the "board/platform boot path" that was supposed to
779 // register never fired for pure-Rust Zephyr images: the zephyr
780 // module emits a STRONG `nros_app_register_backends` stub for the
781 // Kconfig-selected RMW (zephyr/CMakeLists.txt Phase 160.A), but
782 // only the C/C++ `nros_cpp_init` path ever CALLED it — a Rust-only
783 // image reached `Executor::open` with no backend registered and
784 // died with Transport(ConnectionFailed) (silently, pre-0155).
785 // Call the hook explicitly, exactly like the C++ init path.
786 unsafe extern "C" {
787 fn nros_app_register_backends();
788 }
789 unsafe { nros_app_register_backends() };
790 // Issue 0163 — a pure-Rust image has no `libnros_c.a`, so the
791 // backend must ride in THIS staticlib and be referenced from the
792 // app crate, or rustc's staticlib DCE drops the whole backend
793 // closure (the `#[no_mangle]` C export included), leaving the
794 // module's weak `nros_rmw_<x>_register` resolving to NULL and the
795 // hook above registering nothing.
796 //
797 // Issue 0330 — that reference used to be a pair of hardcoded
798 // `::nros_rmw_zenoh::register()` / `::nros_rmw_xrce_cffi::register()`
799 // calls emitted RIGHT HERE, which named two concrete backends in
800 // the RMW-agnostic facade (and left cyclonedds handled asymmetrically
801 // through the C hook). It also forced every consumer to carry
802 // `rmw-zenoh` / `rmw-xrce` feature rows purely so these `cfg`s would
803 // resolve — the cyclonedds-only example carried both as inert
804 // placeholders. The anchor now lives in the app crate, which is the
805 // layer that legitimately selects an RMW: see
806 // [`nros::force_link_backend!`]. Registration itself is unchanged —
807 // the `nros_app_register_backends` hook above does it.
808 // Locator: `default_const()` = EMPTY locator → zenoh-pico
809 // multicast scouting, which native_sim NSOS can't satisfy.
810 // Bake `NROS_LOCATOR` at compile time (the example `build.rs`
811 // re-exports `CONFIG_NROS_ZENOH_LOCATOR` from Kconfig into that
812 // env). No baked value → falls back to the empty locator.
813 const BAKED_LOCATOR: ::core::option::Option<&str> = ::core::option_env!("NROS_LOCATOR");
814 // Domain: the example `build.rs` bakes `CONFIG_NROS_DOMAIN_ID`
815 // into `NROS_DOMAIN_ID` the same way (its comment has promised
816 // this consumption since phase-225; the phase-277 macro rework
817 // dropped it — issue 0161: every Rust cyclonedds image silently
818 // ran domain 0 regardless of the Kconfig bake).
819 const BAKED_DOMAIN: ::core::option::Option<&str> =
820 ::core::option_env!("NROS_DOMAIN_ID");
821 let domain_id: u32 = match BAKED_DOMAIN {
822 ::core::option::Option::Some(d) => match d.parse() {
823 ::core::result::Result::Ok(v) => v,
824 ::core::result::Result::Err(_) => {
825 panic!("nros zephyr entry: NROS_DOMAIN_ID baked non-numeric: {d:?}")
826 }
827 },
828 ::core::option::Option::None => 0,
829 };
830 // #166 / phase-286 W1 — native_sim test parallelism. The test
831 // harness launches the image with `-testargs --nros-locator=<loc>`
832 // and starts a per-test zenohd on that (ephemeral) port; preferring
833 // it over the build-time bake lets every test dial a DISTINCT router,
834 // retiring the shared-baked-port serialization of the zenoh e2e
835 // lanes. Provided by `nros-platform-zephyr` (argv-backed, process
836 // lifetime); returns NULL on real embedded → the bake stands.
837 unsafe extern "C" {
838 fn nros_runtime_locator_override() -> *const ::core::ffi::c_char;
839 }
840 let runtime_locator: ::core::option::Option<&str> = {
841 let p = unsafe { nros_runtime_locator_override() };
842 if p.is_null() {
843 ::core::option::Option::None
844 } else {
845 match unsafe { ::core::ffi::CStr::from_ptr(p) }.to_str() {
846 ::core::result::Result::Ok(s) if !s.is_empty() => {
847 ::core::option::Option::Some(s)
848 }
849 _ => ::core::option::Option::None,
850 }
851 }
852 };
853 let effective_locator = runtime_locator.or(match BAKED_LOCATOR {
854 ::core::option::Option::Some(loc) if !loc.is_empty() => {
855 ::core::option::Option::Some(loc)
856 }
857 _ => ::core::option::Option::None,
858 });
859 let config = match effective_locator {
860 ::core::option::Option::Some(loc) => {
861 $crate::ExecutorConfig::new(loc).node_name(<$node as $crate::Node>::NAME)
862 }
863 ::core::option::Option::None => {
864 $crate::ExecutorConfig::default_const().node_name(<$node as $crate::Node>::NAME)
865 }
866 }
867 .domain_id(domain_id);
868 // Issue 0155 — fail LOUD (repo rule: panic, not silent
869 // early-return). A silent `return` here idles the image with zero
870 // output; the zephyr-cyclonedds rust lane was undiagnosable until
871 // this printed the real error.
872 let executor = match $crate::Executor::open(&config) {
873 Ok(executor) => executor,
874 Err(e) => {
875 panic!("nros zephyr entry: Executor::open failed: {e:?}");
876 }
877 };
878 let mut runtime = $crate::ExecutorNodeRuntime::from_executor(executor);
879 if let Err(e) = runtime.register_node::<$node>() {
880 panic!("nros zephyr entry: register_node failed: {e:?}");
881 }
882 // Readiness marker. The C/C++ Zephyr listeners print
883 // "Waiting for messages..." from their `main()` before the spin
884 // loop; the e2e harness polls for that substring to know the
885 // subscriber has declared before starting the talker (Phase 89.12).
886 // The Rust path's spin loop lives in this macro (the node only owns
887 // callbacks), so emit the same canonical marker here — without it a
888 // fully-working Rust listener never signals readiness and the e2e
889 // times out at 30 s (issue #35: the zenoh native_sim rust pubsub /
890 // service / action failures were this missing marker, not a
891 // transport fault — `Executor::open` + `register_node` had already
892 // succeeded).
893 ::log::info!("Waiting for messages");
894 loop {
895 let _ = runtime.spin_once(::core::time::Duration::from_millis(10));
896 }
897 }
898 };
899}
900
901// Re-export node types
902pub use nros_node::{NodeConfig, PublisherHandle, StandaloneNode, SubscriptionHandle};
903
904// Re-export publisher/subscriber options (topic + QoS; always available).
905pub use nros_node::{PublisherOptions, SubscriptionOptions};
906
907// Re-export timer types
908pub use nros_node::{TimerCallbackFn, TimerDuration, TimerHandle, TimerMode, TimerState};
909// phase-425 W4/W5 — which clock advances a timer. Re-exported here for the
910// reason `sim-time` is a feature here: an application deps `nros`, not
911// `nros-node`, so a capability that stops at the core crate is one no
912// application can name. W4 added the type and the registrar and stopped at the
913// core, which the `/clock` fixture found the moment it tried to use them.
914// Gated on `rmw-cffi`, the spelling every other executor re-export here uses:
915// `nros-node`'s own `has_rmw` IS its `rmw-cffi` feature, and `has_rmw` is a
916// build-script cfg that does not exist in THIS crate.
917#[cfg(feature = "rmw-cffi")]
918pub use nros_node::executor::TimerClockSource;
919
920// Re-export transport types (middleware-agnostic)
921pub use nros_rmw::{
922 ClientTrait, Publisher, QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy,
923 QoSOverride, QoSOverrideRole, QoSOverrideValue, QoSPolicyMask, QoSProfile,
924 QoSReliabilityPolicy, Rmw, RmwConfig, ServiceInfo, ServiceRequest, ServiceTrait, Session,
925 SessionMode, Subscription as SubscriptionTrait, TopicInfo, Transport, TransportConfig,
926};
927
928/// Phase 108.B — standard ROS-2-equivalent QoS profiles. Match
929/// upstream `rmw_qos_profile_default` etc. field-by-field. Backends
930/// validate against these synchronously at create time; no silent
931/// downgrade.
932// phase-379 W5 — rclrs exports the eight QoS presets as CRATE-LEVEL consts
933// (`rclrs::QOS_PROFILE_DEFAULT`); ours were associated consts on `QoSProfile`
934// and nothing re-exported them, so `use rclrs::QOS_PROFILE_DEFAULT` had no
935// counterpart to port to. The NAMES already matched exactly — only the path
936// did not, which is why the ledger filed these as a re-export and not a
937// rename.
938//
939// These ALIAS the associated consts rather than restating them. `nros::qos`
940// below is a second, hand-written copy of the same presets that predates them;
941// `qos_presets_agree` in the test module asserts the two never drift, which is
942// the hand-mirror class (issues 0088/0160/0245) one layer up.
943/// `rmw_qos_profile_default` — reliable, volatile, keep-last(10).
944pub const QOS_PROFILE_DEFAULT: QoSProfile = QoSProfile::QOS_PROFILE_DEFAULT;
945/// `rmw_qos_profile_system_default` — the RMW implementation's own defaults.
946pub const QOS_PROFILE_SYSTEM_DEFAULT: QoSProfile = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT;
947/// `rmw_qos_profile_sensor_data` — best-effort, keep-last(5).
948pub const QOS_PROFILE_SENSOR_DATA: QoSProfile = QoSProfile::QOS_PROFILE_SENSOR_DATA;
949/// `rmw_qos_profile_services_default`.
950pub const QOS_PROFILE_SERVICES_DEFAULT: QoSProfile = QoSProfile::QOS_PROFILE_SERVICES_DEFAULT;
951/// `rmw_qos_profile_parameters` — reliable, depth 1000.
952pub const QOS_PROFILE_PARAMETERS: QoSProfile = QoSProfile::QOS_PROFILE_PARAMETERS;
953/// `rmw_qos_profile_parameter_events`.
954pub const QOS_PROFILE_PARAMETER_EVENTS: QoSProfile = QoSProfile::QOS_PROFILE_PARAMETER_EVENTS;
955/// The clock preset — sensor-data shaped with depth 1.
956pub const QOS_PROFILE_CLOCK: QoSProfile = QoSProfile::QOS_PROFILE_CLOCK;
957/// The action-status preset — reliable + transient-local, depth 1.
958pub const QOS_PROFILE_ACTION_STATUS_DEFAULT: QoSProfile =
959 QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT;
960
961pub mod qos {
962 use crate::{
963 QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy, QoSProfile,
964 QoSReliabilityPolicy,
965 };
966
967 /// `rmw_qos_profile_default`-equivalent: reliable + volatile +
968 /// keep-last(10), automatic liveliness, no deadline / lifespan.
969 pub const DEFAULT: QoSProfile = QoSProfile {
970 reliability: QoSReliabilityPolicy::Reliable,
971 durability: QoSDurabilityPolicy::Volatile,
972 history: QoSHistoryPolicy::KeepLast,
973 liveliness_kind: QoSLivelinessPolicy::Automatic,
974 depth: 10,
975 deadline_ms: 0,
976 lifespan_ms: 0,
977 liveliness_lease_ms: 0,
978 avoid_ros_namespace_conventions: false,
979 tx_express: false,
980 };
981
982 /// `rmw_qos_profile_sensor_data`-equivalent: best-effort +
983 /// volatile + keep-last(5).
984 pub const SENSOR_DATA: QoSProfile = QoSProfile {
985 reliability: QoSReliabilityPolicy::BestEffort,
986 depth: 5,
987 ..DEFAULT
988 };
989
990 /// `rmw_qos_profile_services_default`-equivalent.
991 pub const SERVICES_DEFAULT: QoSProfile = DEFAULT;
992
993 /// `rmw_qos_profile_parameters`-equivalent: depth = 1000.
994 pub const PARAMETERS: QoSProfile = QoSProfile {
995 depth: 1000,
996 ..DEFAULT
997 };
998
999 /// `rmw_qos_profile_system_default`-equivalent — **an absence, not a
1000 /// profile**: every policy is the SYSTEM_DEFAULT sentinel and the depth is
1001 /// 0, resolved by whichever backend is linked.
1002 ///
1003 /// issue 0829 — this said `= DEFAULT` (depth 10) while
1004 /// `QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT` said depth 1, so one name
1005 /// shipped two queue depths depending on which spelling a caller reached.
1006 /// It is now an ALIAS of the associated const, like the other four, and
1007 /// neither number survives: see `QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT`.
1008 pub const SYSTEM_DEFAULT: QoSProfile = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT;
1009}
1010
1011// Re-export safety types when feature is enabled
1012#[cfg(feature = "safety-e2e")]
1013pub use nros_rmw::{IntegrityStatus, SafetyValidator, crc32};
1014
1015// Phase 248 C7 step 1 — the `nros::platform::zephyr` module (the
1016// `wait_for_network` FFI wrapper) RELOCATED to `nros-platform`
1017// (`nros_platform::zephyr::wait_network`); callers reference it via
1018// `::nros_platform::zephyr::wait_network`. nros no longer hosts a platform
1019// helper module. (The `zephyr_component_main!` macro relocation is C7 step 2.)
1020//
1021/// Backend-specific internal types.
1022///
1023/// These types are implementation details of the transport backends.
1024/// Most users should use the high-level APIs (`Executor`, etc.)
1025/// instead of these types directly.
1026///
1027/// The `Rmw*` type aliases resolve to whichever backend is active at compile time,
1028/// providing a backend-agnostic way to reference concrete transport types.
1029pub mod internals {
1030 // ── Backend-agnostic type aliases ────────────────────────────────────
1031 // These resolve to the concrete types of the active RMW backend.
1032 // Today the only exposed backend at this layer is the cffi shim.
1033
1034 #[cfg(feature = "rmw-cffi")]
1035 pub type RmwSession = nros_rmw_cffi::CffiSession;
1036 #[cfg(feature = "rmw-cffi")]
1037 pub type RmwPublisher = nros_rmw_cffi::CffiPublisher;
1038 #[cfg(feature = "rmw-cffi")]
1039 pub type RmwSubscriber = nros_rmw_cffi::CffiSubscription;
1040 #[cfg(feature = "rmw-cffi")]
1041 pub type RmwServiceServer = nros_rmw_cffi::CffiService;
1042 #[cfg(feature = "rmw-cffi")]
1043 pub type RmwServiceClient = nros_rmw_cffi::CffiClient;
1044
1045 /// Phase 124.A — zero-copy publisher slot type. Lives in the
1046 /// `internals` module so `nros-c` can construct + transmute the
1047 /// lifetime when boxing the slot for the C-side `_loan` /
1048 /// `_commit` / `_discard` token plumbing.
1049 #[cfg(all(feature = "rmw-cffi", feature = "lending"))]
1050 pub type RmwSlot<'a> = nros_rmw_cffi::CffiSlot<'a>;
1051
1052 /// Phase 124.A — zero-copy subscriber view type.
1053 #[cfg(all(feature = "rmw-cffi", feature = "lending"))]
1054 pub type RmwView<'a> = nros_rmw_cffi::CffiView<'a>;
1055
1056 /// Open a new middleware session.
1057 ///
1058 /// Wraps the backend-specific session constructor behind a common signature.
1059 /// Used by the C API (`nros-c`); Rust users should prefer `Executor::open()`.
1060 ///
1061 /// Phase 156 — takes an explicit primary backend by name, mirroring what
1062 /// `Executor::open` does for Rust callers. Without it, C bridges built with
1063 /// two linked backends (e.g. xrce + dds) get whichever ctor fires first —
1064 /// non-deterministic across link orderings, and often the wrong backend for
1065 /// the bridge's intended primary.
1066 ///
1067 /// Issue 1050 defect (3) — `rmw` is the RESOLVED selector, not a second
1068 /// reading of the environment. This function used to consult
1069 /// `rmw_selector()` itself, which made it the tree's second answer to "which
1070 /// backend?" and blind to every rung but the environment: a C image with a
1071 /// BAKED selector had it discarded here. The caller resolves the whole
1072 /// ladder (`env > baked > none`) through `ExecutorConfig` and passes the
1073 /// result; `None` means the registry must hold exactly one backend, which
1074 /// `nros_rmw_cffi::get_vtable` now enforces rather than assumes.
1075 #[cfg(feature = "rmw-cffi")]
1076 pub fn open_session(
1077 locator: &str,
1078 mode: nros_rmw::SessionMode,
1079 domain_id: u32,
1080 node_name: &str,
1081 rmw: Option<&str>,
1082 ) -> Result<RmwSession, nros_rmw::TransportError> {
1083 use nros_rmw::Rmw;
1084
1085 // Phase 249 P4b.1 — every linked backend self-registered via
1086 // its `.init_array` ctor before `main` (RFC-0042 §D3.3); no
1087 // runtime section walk.
1088
1089 let config = nros_rmw::RmwConfig {
1090 locator,
1091 mode,
1092 domain_id,
1093 node_name,
1094 namespace: "",
1095 properties: &[],
1096 };
1097 // Phase 155.B — propagate the real `TransportError` instead of
1098 // collapsing every backend failure to `ConnectionFailed`. The
1099 // C-side `nros_support_init` decodes the variant into a
1100 // specific `NROS_RET_*` code so "init -> -X" tells the user
1101 // which precondition the backend rejected.
1102 // issue 1050 defect (3) — the selector ARRIVES here now. It used to be
1103 // read from `$NROS_RMW` on the spot, which is why a baked one could not
1104 // reach this path: the reader knew about one rung and the resolver knew
1105 // about two. The environment still wins; it wins in `ExecutorConfig`,
1106 // where every other field's precedence is decided, instead of here.
1107 if let Some(name) = rmw {
1108 return nros_rmw_cffi::CffiRmw::open_with_rmw(name, &config);
1109 }
1110 nros_rmw_cffi::CffiRmw.open(&config)
1111 }
1112
1113 /// Drive middleware I/O for pull-based backends.
1114 ///
1115 /// Delegates to [`Session::drive_io()`](nros_rmw::Session::drive_io),
1116 /// which each backend implements appropriately (no-op for push-based,
1117 /// poll for pull-based).
1118 ///
1119 /// Used by the C API executor before polling handles.
1120 #[cfg(feature = "rmw-cffi")]
1121 pub fn drive_session_io(session: &mut RmwSession, timeout_ms: i32) {
1122 use nros_rmw::Session;
1123 let _ = session.drive_io(timeout_ms);
1124 }
1125}
1126
1127// Re-export types that don't depend on RMW (always available)
1128pub use nros_node::{
1129 BOOT_SET_DOMAIN, BOOT_SET_LOCATOR, BOOT_SET_NAMESPACE, BOOT_SET_NODE_NAME, BakedBootConfig,
1130 BootConfig, BootConfigError, DOMAIN_ID_EXPLICIT_ZERO_C_ABI, DOMAIN_ID_MAX, ExecutorConfig,
1131 ExecutorSemantics, GuardCondition, HandleId, HandleSet, InvocationMode, NROS_BOOT_CONFIG_MAGIC,
1132 NROS_BOOT_CONFIG_VERSION, RawCancelCallback, RawGoalCallback, RawServiceCallback,
1133 RawSubscriptionCallback, ReadinessSnapshot, ShutdownCallbackFn, ShutdownCallbackHandle,
1134 ShutdownPhase, SpinOnceResult, SpinOptions, SpinPeriodPollingResult, Trigger,
1135 baked_domain_from_c_abi,
1136};
1137
1138// ---------------------------------------------------------------------------
1139// The error vocabulary (issue 0783). Both re-exports are unconditional.
1140//
1141// Every fallible call in the Rust user API returns `NodeError`, and
1142// `NodeError::Transport(TransportError)` is its most common variant — so
1143// handling one means naming both. They used to be exported from two unrelated
1144// blocks (`NodeError` with the boot/executor types, `TransportError` with the
1145// transport traits), which is a discoverability nit rather than a capability
1146// gap: both were always reachable, and `prelude` already listed them together.
1147// Nothing here changes what `nros::` exports.
1148//
1149// There is deliberately NO numeric code type beside them. A Rust caller matches
1150// a flat enum; the numeric vocabulary is the C ABI's `nros_ret_t` /
1151// `NROS_RET_*` (0, -1..-16), which is its own space and not `rcl_ret_t`'s.
1152// nros-core carried an `rcl_ret_t` mirror (`RclReturnCode`) and an error that
1153// wrapped it (`NanoRosError`); neither was ever reachable from this facade and
1154// neither had a producer, so issue 0783 deleted them rather than exporting a
1155// type a user could name and never receive. RFC-0036's Errors row now describes
1156// what these two are.
1157// ---------------------------------------------------------------------------
1158pub use nros_node::NodeError;
1159pub use nros_rmw::TransportError;
1160
1161// RFC-0052 / phase-296 W3b — on-target contract-monitor types. Baked
1162// `system_monitors.rs` uses the fully-qualified `::nros_node::executor::
1163// monitor::*` path; this re-export lets hand-written entries and fixtures
1164// reach the same types through the `nros` umbrella (they install the
1165// tables via `Executor::set_monitor_table` / `set_age_table` and drain
1166// with `drain_violations`). The monitor module is `has_rmw`-gated in
1167// nros-node (it names entity types), so mirror that with `rmw-cffi`.
1168#[cfg(feature = "rmw-cffi")]
1169pub mod monitor {
1170 pub use nros_node::executor::monitor::{
1171 AgeMonitorSpec, MonitorSpec, PubMonitorCell, SubMonitorCell, Violation,
1172 };
1173}
1174
1175// Re-export RMW-dependent types (require an active transport backend)
1176#[cfg(feature = "rmw-cffi")]
1177pub use nros_node::{
1178 ActionClient, ActionClientCore, ActionServer, ActionServerCore, ActionServerHandle,
1179 ActionServerRawHandle, ActiveGoal, CompletedGoal, EmbeddedPublisher, EmbeddedRawPublisher,
1180 EmbeddedServiceClient, EmbeddedServiceServer, Executor, ExecutorSizing, FeedbackStream,
1181 GoalFeedbackStream, LoanError, NodeHandle, Promise, PublishLoan, RawActionClientSpec,
1182 RawActionServerSpec, RawActiveGoal, RawSubscription, RecvView, SessionHandle, SessionSpec,
1183 Subscription, action_channel_type,
1184};
1185
1186// phase-271 (issue #110) — per-entry executor sizing helper: the orchestration
1187// codegen's `build_executor` sizes its backing to the system's callback count
1188// via `nros::arena_size_for(CALLBACK_COUNT)` + `ExecutorSizing`, replacing the
1189// workspace-global `NROS_EXECUTOR_MAX_CBS`.
1190#[cfg(feature = "rmw-cffi")]
1191pub use nros_node::config::arena_size_for;
1192
1193/// The configured default receive-buffer size (`NROS_SUBSCRIPTION_BUFFER_SIZE`).
1194///
1195/// NOT a fallback for a missing bound any more (phase-403 W0): [`rx_buffer_for!`]
1196/// used to expand to this for an unbounded type and now refuses to compile
1197/// instead. It stays public, and stays the default, for the paths that have no
1198/// `M` to ask rather than a bound they declined to use -- `create_subscription_raw`
1199/// and the RFC-0043 type-name-string subscriptions, the service / action / TX
1200/// buffer defaults, the `pubsub_entry` term of the arena derivation in
1201/// `nros-node/build.rs`, and its weld to the C API's `MESSAGE_BUFFER_SIZE`.
1202/// Whether the arena derivation should stop leaning on it is a phase-403 W5
1203/// question.
1204///
1205/// Re-exported here (rather than reached through `nros_node`) because a macro
1206/// expands at the CALLER, where `nros_node` may not be a dependency at all.
1207pub use nros_node::config::DEFAULT_RX_BUF_SIZE;
1208
1209/// Phase 392 W3b — the bound behind [`rx_buffer_for!`]. Not part of the stable
1210/// surface; call the macro.
1211///
1212/// Public only because a `macro_rules!` body is expanded in the caller's crate
1213/// and can reach nothing private. `#[doc(hidden)]` and `__`-prefixed for the
1214/// same reason every other macro-support item in this crate is.
1215#[doc(hidden)]
1216pub const fn __rx_bound<M: nros_serdes::schema::Message>() -> Option<usize> {
1217 nros_serdes::size::max_serialized_bound::<M>()
1218}
1219
1220// Phase 173.5 — board config traits. `BoardConfig` (read locator /
1221// domain). `BoardTransportConfig` was removed with its dead
1222// setters (issue 1064); the live path is the deploy overlay.
1223pub use nros_platform::BoardConfig;
1224
1225// Phase 216.A.1 — `DispatchStrategy` enum. User-visible at
1226// `nros::DispatchStrategy`; the canonical home is `nros_platform::
1227// board::dispatch` so the C ABI symbol the `nros::node!()` macro emits
1228// (`__nros_node_<pkg>_dispatch_strategy() -> u8`) lives next to the
1229// other board-side trampolines.
1230pub use nros_platform::DispatchStrategy;
1231
1232/// Implementation detail — used by `nros::node!()` macro expansion.
1233///
1234/// Re-exports `nros_platform` so the macro's emitted trampoline can
1235/// reference `RuntimeCtx` / `RuntimeError` / the `Node*Fn`
1236/// fn-pointer aliases without forcing every consumer Node pkg's
1237/// `Cargo.toml` to carry an explicit `nros-platform` dep on top of
1238/// `nros`. Phase 212.M-F.13 path (b).
1239///
1240/// Not part of the public API — paths under this module may change at
1241/// any time. End users should depend on `nros` alone and invoke
1242/// `nros::node!()`; the macro routes through here automatically.
1243#[doc(hidden)]
1244pub mod __macro_support {
1245 pub use ::nros_platform;
1246
1247 /// phase-314 — whether THIS `nros` build carries the parameter services.
1248 ///
1249 /// The `nros::main!` expansion const-asserts it when the system declares
1250 /// `[param_services]`. A cfg in the entry crate cannot see this: the
1251 /// feature belongs to `nros`, and the entry enables it through its
1252 /// dependency, so only `nros` itself can report the answer.
1253 ///
1254 /// Without the assert the mismatch is SILENT — `apply_param_services` is a
1255 /// no-op, the build succeeds, the image boots, and `ros2 param list`
1256 /// returns nothing.
1257 pub const PARAM_SERVICES_ENABLED: bool = cfg!(feature = "param-services");
1258
1259 /// Issue 0257 — the build-time executor callback-table size
1260 /// (`NROS_EXECUTOR_MAX_CBS`, default 4). Re-exported so the `nros::main!`
1261 /// expansion can `const`-assert the model's entity count against the
1262 /// capacity that ACTUALLY compiles in, instead of letting the image boot
1263 /// and die on `create_timer (code=-6 Full)`.
1264 pub use ::nros_node::config::MAX_CBS as EXECUTOR_MAX_CBS;
1265}
1266
1267// Phase 110.B / 110.G — scheduling-context API surface. Consumers
1268// of the Phase 110 cyclic / TT scheduler need these types to
1269// describe schedules and bind handles; re-exporting them here
1270// keeps user code free of `nros_node::executor::sched_context`
1271// path noise. Gated on `rmw-cffi`: the source module is
1272// `#[cfg(any(has_rmw, test))]` in nros-node, so it only exists once
1273// an RMW backend is linked (matches the re-export block above).
1274#[cfg(feature = "rmw-cffi")]
1275pub use nros_node::executor::sched_context::{
1276 DeadlineAction, DeadlinePolicy, OptUs, Priority, SchedClass, SchedContext, SchedContextId,
1277 TimeTriggeredSchedule, TimeTriggeredScheduleError, TimeTriggeredWindow,
1278};
1279
1280#[cfg(all(feature = "std", feature = "rmw-cffi"))]
1281pub use nros_node::SpinPeriodResult;
1282
1283// Re-export service types
1284pub use nros_core::{ServiceClient, ServiceServer};
1285
1286// Re-export action types.
1287//
1288// issue 0796 — `CancelResponse` (the per-goal Reject/Accept decision) and
1289// `CancelReturnCode` (the `action_msgs/srv/CancelGoal` RPC status) are two
1290// concepts that shared one name until the split. Both are exported: without
1291// `CancelReturnCode` here, `ActionClient::cancel_goal`'s `Promise<CancelReturnCode>`
1292// could not be NAMED from `nros::` alone.
1293pub use nros_core::{
1294 CancelResponse, CancelReturnCode, GoalId, GoalInfo, GoalResponse, GoalStatus,
1295 GoalStatusStamped, RosAction,
1296};
1297
1298// Re-export lifecycle types (always available, no_std compatible)
1299pub use nros_core::{LifecycleState, LifecycleTransition, TransitionResult};
1300pub use nros_node::{LifecycleCallbackFn, LifecycleError, LifecyclePollingNode};
1301
1302/// Re-export of the full lifecycle module so examples can reach
1303/// `LifecycleCallbackSlot`, `LifecyclePollingNodeCtx`, etc.
1304pub mod lifecycle {
1305 pub use nros_core::lifecycle::{LifecycleState, LifecycleTransition, TransitionResult};
1306 pub use nros_node::lifecycle::*;
1307}
1308
1309// Phase 128.G — bridge surface re-exports. Gated behind the
1310// `bridge` / `config` umbrella features so single-backend builds
1311// don't pull in `nros-bridge` (or, for `config`, the TOML stack).
1312#[cfg(feature = "bridge")]
1313pub use nros_bridge as bridge;
1314
1315#[cfg(feature = "config")]
1316pub use nros_bridge::run_from_config;
1317
1318// Re-export parameter types.
1319//
1320// phase-382 W2' — `ParameterStorage` / `ParameterTable` are here because the
1321// store's slots are CALLER-OWNED: anyone constructing a `ParameterServer`
1322// outside an executor has to place the storage and lend it.
1323pub use nros_params::{
1324 MandatoryParameter, OptionalParameter, Parameter, ParameterBuilder, ParameterDescriptor,
1325 ParameterError, ParameterServer, ParameterStorage, ParameterTable, ParameterType,
1326 ParameterValue, ParameterVariant, ReadOnlyParameter, SetParameterResult,
1327};
1328/// Prelude module for convenient imports
1329///
1330/// Import everything you need with a single statement:
1331/// ```
1332/// use nros::prelude::*;
1333/// ```
1334/// phase-379 W5 — the RTOS machinery, named explicitly.
1335///
1336/// The second tier of the two-tier surface. `nros::prelude` is the API a ported
1337/// ROS 2 node uses; everything here exists because the target is an RTOS, has no
1338/// correspondent in rclrs/rclcpp/rclc, and a ROS 2 developer reading a node
1339/// should not have to step over it.
1340///
1341/// Nothing moved out of `nros::` — these are re-exports, and every name is still
1342/// reachable at its old path. What changed is that they are no longer dragged in
1343/// by `use nros::prelude::*`.
1344///
1345/// The membership rule is mechanical rather than taste: a name belongs in the
1346/// PRELUDE iff the parity ledger gives it a non-`extension` verdict — i.e. it
1347/// corresponds to something in rclrs, rclcpp or rclc. Names with no
1348/// correspondent belong here, unless they are load-bearing for startup
1349/// (`ExecutorConfig`, `SpinOptions`), which is an argued allow-list rather than
1350/// an exception anyone may grow.
1351pub mod embedded {
1352 // Wire encoding. A node publishes typed messages; these are for code that
1353 // handles bytes, which upstream hides entirely.
1354 pub use crate::{CdrReader, CdrWriter};
1355
1356 // Handle bookkeeping — the static tables that replace an allocator.
1357 #[cfg(feature = "rmw-cffi")]
1358 pub use crate::{HandleId, HandleSet, InvocationMode};
1359
1360 // Component/runtime plumbing, and the source-metadata capture the
1361 // orchestration layer records. None of it appears in a ported node.
1362 pub use crate::{MetadataRecorder, NodeRuntimeAdapter, RuntimeNodeRecord};
1363
1364 // Entity REGISTRATION vocabulary. A ported node writes
1365 // `create_publisher(...)`; these are what the declaration macros and the
1366 // orchestration layer use to describe what was created.
1367 pub use crate::{
1368 ActionTag, Callback, CallbackEffectKind, CallbackEffects, DeclaredNode,
1369 DeclaredNodeRuntime, EntityKind, NodeRuntime, ServiceTag, SourceLocationMetadata,
1370 SourceNameKind, SubscriptionTag, record_node_metadata, register_node,
1371 };
1372 // Gated where the root gates it — the export follows the capability, not
1373 // the tier.
1374 #[cfg(feature = "alloc")]
1375 pub use crate::SourceMetadataExport;
1376}
1377
1378pub mod prelude {
1379 // phase-379 W5 — the RTOS machinery moved to `nros::embedded`. Removed
1380 // here rather than re-exported from both: a two-tier surface that still
1381 // drags tier two in through the glob is one tier with extra words.
1382
1383 pub use crate::{
1384 Deserialize, Logger, MessageInfo, NodeConfig, PublisherHandle, QoSDurabilityPolicy,
1385 QoSHistoryPolicy, QoSProfile, QoSReliabilityPolicy, RosMessage, RosService, Serialize,
1386 StandaloneNode, SubscriptionHandle, TopicInfo,
1387 };
1388
1389 // Re-export component-mode API.
1390 #[cfg(feature = "rmw-cffi")]
1391 pub use crate::NodeExecutorRuntime;
1392 #[cfg(feature = "alloc")]
1393 pub use crate::{
1394 Node, NodeActionClient, NodeActionServer, NodeContext, NodeDeclError, NodeOptions,
1395 NodeParameter, NodePublisher, NodeResult, NodeServiceClient, NodeServiceServer,
1396 NodeSubscription, NodeTimer, ParameterDefault, node,
1397 };
1398
1399 // Re-export lifecycle types
1400 pub use crate::{
1401 LifecycleCallbackFn, LifecycleError, LifecyclePollingNode, LifecycleState,
1402 LifecycleTransition, TransitionResult,
1403 };
1404
1405 // Re-export executor config + handle types (always available)
1406 pub use crate::{
1407 ExecutorConfig, GuardCondition, NodeError, SessionMode, SpinOnceResult, SpinOptions,
1408 SpinPeriodPollingResult, TransportError, Trigger,
1409 };
1410
1411 // issue 0687 — `ExecutorConfig::from_env()` is an extension trait now (the
1412 // environment is read at this crate's edge, not in the core), so the
1413 // spelling only works where the trait is in scope. It is in the prelude
1414 // precisely so that the consumers written against the inherent method —
1415 // the native examples, the benches — keep compiling unchanged.
1416 #[cfg(feature = "env")]
1417 pub use crate::ExecutorConfigEnvExt;
1418
1419 // Re-export RMW-dependent executor + handle types
1420 #[cfg(feature = "rmw-cffi")]
1421 pub use crate::{
1422 EmbeddedPublisher, EmbeddedServiceClient, Executor, FeedbackStream, NodeHandle, Promise,
1423 Subscription,
1424 };
1425
1426 // Publisher/Subscriber options (topic + QoS).
1427 pub use crate::{PublisherOptions, SubscriptionOptions};
1428
1429 #[cfg(all(feature = "std", feature = "rmw-cffi"))]
1430 pub use crate::SpinPeriodResult;
1431
1432 // Re-export parameter types
1433 pub use crate::{ParameterServer, ParameterStorage, ParameterType, ParameterValue};
1434
1435 // Re-export typed parameter API (rclrs-compatible builder pattern)
1436 pub use crate::{
1437 MandatoryParameter, OptionalParameter, ParameterBuilder, ParameterError, ParameterVariant,
1438 ReadOnlyParameter,
1439 };
1440
1441 // Re-export action types
1442 pub use crate::{GoalId, GoalInfo, GoalResponse, GoalStatus, GoalStatusStamped, RosAction};
1443
1444 // Re-export Time, Duration, Clock from core
1445 pub use nros_core::{Clock, ClockType, Duration, Time};
1446
1447 // Re-export timer types
1448 pub use crate::{TimerCallbackFn, TimerDuration, TimerHandle, TimerMode};
1449}
1450
1451/// Derive macros for message types
1452///
1453/// Use these macros to generate message serialization code.
1454/// These macros help you create custom message types that are compatible
1455/// with ROS 2's CDR serialization format.
1456pub mod derive {
1457 #[cfg(feature = "macros")]
1458 pub use nros_macros::RosMessage;
1459}
1460
1461#[cfg(test)]
1462mod tests {
1463 #[test]
1464 fn test_prelude_imports() {
1465 // This test just verifies that the prelude compiles
1466 use crate::prelude::*;
1467
1468 let _ = NodeConfig::new("test_node", "/");
1469 let _ = QoSProfile::BEST_EFFORT;
1470 }
1471
1472 /// Verify the Node* canonical trait + context + result types
1473 /// resolve after the Component→Node hard rename. The Component*
1474 /// aliases were dropped in the same phase; their absence is
1475 /// enforced by the workspace audit (no live `Component*` ident
1476 /// remains in core / examples / tests).
1477 #[test]
1478 fn node_context_types_resolve() {
1479 // Canonical "Node*" trait + context names (post-rename).
1480 fn _take_node_ctx<N: crate::Node>(_: &mut crate::NodeContext<'_, dyn crate::NodeRuntime>) {}
1481 // Result type resolves.
1482 let _: crate::NodeResult<()> = Ok(());
1483 }
1484}
1485
1486// ---------------------------------------------------------------------------
1487// phase-361 W8.e / issue 0594 — capabilities REQUIRE the heap / the standard
1488// library, they do not enable it. Turning `alloc` or `std` on for the user
1489// silently changes what their firmware image is; naming the feature they must
1490// add does not.
1491// ---------------------------------------------------------------------------
1492// issue 0687 follow-up — `alloc`, not `std`. The reason recorded here ("writes
1493// a file and exits") was never this crate's: the file write is `nros-cpp`'s
1494// `metadata_hooks`. What `metadata_mode.rs` actually needed was a `Sync`
1495// global, and it named `std::sync::Mutex` for it; on the portable mutex the
1496// capability is `String` + `format!` + a lock, i.e. the heap and nothing more.
1497#[cfg(all(feature = "metadata-mode", not(feature = "alloc")))]
1498compile_error!(
1499 "`metadata-mode` records into a heap-allocated global: add \"alloc\" to this crate's features"
1500);
1501// Emitted here as well as in `nros-node`, and NOT for the reason it first
1502// looks like. `nros = { features = ["env"] }` alone does not reach this line:
1503// `env` forwards to `nros-node/env`, nros-node is compiled first, and its own
1504// guard aborts the build there — measured, not assumed. What this covers is
1505// the case feature unification creates, where some other crate in the graph
1506// turns `nros-node/std` on so that guard stays quiet while THIS crate's `std`
1507// is still off. Rare, and exactly the shape that would otherwise compile a
1508// hosted capability into a build that never named the standard library.
1509#[cfg(all(feature = "env", not(feature = "std")))]
1510compile_error!(
1511 "`env` reads the process environment, which needs the standard library: add \"std\" to this crate's features"
1512);
1513
1514// phase-379 W5 — the crate-level `QOS_PROFILE_*` presets (rclrs parity) and the
1515// older hand-written `nros::qos::*` module are two spellings of the same eight
1516// profiles. The first ALIASES `QoSProfile`'s associated consts; the second
1517// restates them as struct literals and predates them.
1518//
1519// They agree today. Nothing made them agree tomorrow, and a hand-mirrored
1520// constant drifting silently is the class issues 0088 / 0160 / 0245 all record.
1521// So assert it, at compile time where possible.
1522#[cfg(test)]
1523mod qos_preset_parity {
1524 use super::*;
1525
1526 #[test]
1527 fn crate_level_presets_alias_the_associated_consts() {
1528 assert_eq!(QOS_PROFILE_DEFAULT, QoSProfile::QOS_PROFILE_DEFAULT);
1529 assert_eq!(QOS_PROFILE_SENSOR_DATA, QoSProfile::QOS_PROFILE_SENSOR_DATA);
1530 assert_eq!(QOS_PROFILE_PARAMETERS, QoSProfile::QOS_PROFILE_PARAMETERS);
1531 assert_eq!(
1532 QOS_PROFILE_ACTION_STATUS_DEFAULT,
1533 QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT
1534 );
1535 }
1536
1537 /// The one that can actually rot: `qos::*` is a SEPARATE hand-written copy.
1538 #[test]
1539 fn qos_module_agrees_with_the_presets() {
1540 // issue 0829 FIXED — SYSTEM_DEFAULT joins the four that always agreed.
1541 // It is here rather than in its own pinned test because the two copies
1542 // no longer say anything a copy could get wrong: both alias the one
1543 // associated const, which is all sentinel.
1544 assert_eq!(
1545 qos::SYSTEM_DEFAULT,
1546 QOS_PROFILE_SYSTEM_DEFAULT,
1547 "qos::SYSTEM_DEFAULT drifted"
1548 );
1549 assert_eq!(qos::DEFAULT, QOS_PROFILE_DEFAULT, "qos::DEFAULT drifted");
1550 assert_eq!(
1551 qos::SENSOR_DATA,
1552 QOS_PROFILE_SENSOR_DATA,
1553 "qos::SENSOR_DATA drifted"
1554 );
1555 assert_eq!(
1556 qos::SERVICES_DEFAULT,
1557 QOS_PROFILE_SERVICES_DEFAULT,
1558 "qos::SERVICES_DEFAULT drifted"
1559 );
1560 assert_eq!(
1561 qos::PARAMETERS,
1562 QOS_PROFILE_PARAMETERS,
1563 "qos::PARAMETERS drifted"
1564 );
1565 }
1566
1567 /// issue 0829, RESOLVED — this test used to PIN the divergence, asserting
1568 /// depth 10 on the façade side and depth 1 on the `nros-rmw` side because
1569 /// neither was obviously the live one. Both numbers are gone, and the
1570 /// replacement is not "we picked one": no concrete depth can be right,
1571 /// because the two reference RMWs resolve the same sentinel differently
1572 /// (`rmw_cyclonedds_cpp` → `KEEP_LAST, 1`; `rmw_zenoh_cpp` →
1573 /// `RMW_ZENOH_DEFAULT_HISTORY_DEPTH`, 42).
1574 ///
1575 /// So what is asserted now is the SHAPE: `SYSTEM_DEFAULT` states nothing,
1576 /// on every field. That is what makes it different from `DEFAULT` — the
1577 /// two being byte-identical was the older defect, and asserting they
1578 /// DIFFER is what keeps anyone from quietly aliasing them again.
1579 #[test]
1580 fn system_default_states_nothing_on_every_field() {
1581 use nros_rmw::{
1582 DEPTH_SYSTEM_DEFAULT, QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy,
1583 QoSReliabilityPolicy,
1584 };
1585 let sd = QOS_PROFILE_SYSTEM_DEFAULT;
1586 assert_eq!(sd.reliability, QoSReliabilityPolicy::SystemDefault);
1587 assert_eq!(sd.durability, QoSDurabilityPolicy::SystemDefault);
1588 assert_eq!(sd.history, QoSHistoryPolicy::SystemDefault);
1589 assert_eq!(sd.depth, DEPTH_SYSTEM_DEFAULT);
1590 // `None` IS the liveliness sentinel — it lowers to
1591 // `NROS_RMW_LIVELINESS_SYSTEM_DEFAULT` (0), the two having collapsed
1592 // onto one value in phase-376 W5/B2.
1593 assert_eq!(sd.liveliness_kind, QoSLivelinessPolicy::None);
1594 assert!(sd.has_unresolved_system_default());
1595
1596 // The whole point of the name: it is NOT a synonym for DEFAULT.
1597 assert_ne!(
1598 sd, QOS_PROFILE_DEFAULT,
1599 "SYSTEM_DEFAULT aliased DEFAULT again — see issue 0829"
1600 );
1601 assert!(!QOS_PROFILE_DEFAULT.has_unresolved_system_default());
1602 }
1603}