Skip to main content

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}