nros_node/executor/mod.rs
1//! Embedded executor with build-time configured arena.
2//!
3//! Provides `Executor` and `Node` that work with the compile-time
4//! selected RMW backend (zenoh, XRCE-DDS, or C FFI).
5//!
6//! # Example
7//!
8//! ```ignore
9//! use nros_node::executor::*;
10//! use std_msgs::msg::Int32;
11//!
12//! let config = ExecutorConfig::from_env().node_name("my_node");
13//! let mut executor = Executor::open(&config)?;
14//! let mut node = executor.create_node("my_node")?;
15//!
16//! let publisher = node.create_publisher::<Int32>("/chatter")?;
17//! publisher.publish(&Int32 { data: 42 })?;
18//!
19//! loop {
20//! executor.spin_once(core::time::Duration::from_millis(10));
21//! }
22//! ```
23
24#[cfg(any(has_rmw, test))]
25pub mod action_core;
26#[cfg(any(has_rmw, test))]
27mod arena;
28// phase-412 #4 — the compile-time half of "is the arena big enough?". Not
29// `has_rmw`-gated: it reads only `config`, and the check it carries must run in
30// every compilation of this crate, whatever the backend.
31pub mod arena_oracle;
32// phase-392 W6 — the named `.bss` static the `alloc` convenience constructors
33// serve their backing from. `alloc`-gated because it exists to replace a
34// `Box::leak`: a no-alloc entry already supplies its own `static` through
35// `open_in`, and reserving a second one there would be pure waste in exactly
36// the images this campaign is about.
37#[cfg(all(any(has_rmw, test), feature = "alloc"))]
38mod backing;
39// issue 1571 — the spawned tiers' half of the same machinery: the ENTRY owns
40// the static (it knows the tier count), boards hand each tier a slot.
41#[cfg(all(any(has_rmw, test), feature = "alloc"))]
42pub use backing::{
43 EXECUTOR_BACKING_DEFAULT_U64S, TIER_TASK_TCB_U64S, TierBackingShort, TierExecutorBacking,
44 TierExecutorBackingSlot, TierSetupGate, TierSetupGuard, TierTaskMemory, TierTaskMemoryRaw,
45 TierTaskMemorySet, check_tier_executor_backing,
46};
47// Phase 8 (autoware-safety-island `docs/design/callback_tracing.rst`) —
48// callback-level dispatch tracing. Same gating shape as `wake_probe`, for the
49// same reason: hot-path hooks that must vanish in production. The module ALSO
50// self-gates (`#![cfg(feature = "trace-callbacks")]`), so the call sites use
51// paired stubs rather than naming it directly.
52#[cfg(all(any(has_rmw, test), feature = "trace-callbacks"))]
53pub mod callback_trace;
54// phase-463 W7 — the host profile (`profile-mode`, host-only: it needs `std`
55// and no RTOS umbrella resolves it).
56#[cfg(all(any(has_rmw, test), feature = "profile-mode"))]
57pub mod profile;
58// phase-457 W5 (issue 1522) — the shape a DECLARED subscription lowers to.
59// UNGATED on purpose: the executor half needs an RMW seam, but the recorder
60// half runs in a probe that opens no executor, and both must read the same
61// enum.
62pub mod declared_shape;
63#[cfg(any(has_rmw, test))]
64mod handles;
65// issue 0669 / phase-359 W10 — `pub mod handoff` stood here. `Handoff<M, N>`
66// was optional sugar over `Arc<Mutex<heapless::Vec<M, N>>>` for the
67// cross-priority bridge pattern (phase-104.E.3), it was `std`-gated on
68// `std::sync::Mutex` alone, and it had no consumer anywhere in the tree — not
69// one call site in `packages/` or `examples/` since it landed. Deleted rather
70// than ported: the only portable mutex reachable from here costs a `spin`
71// dependency edge on every build of this crate (measured: `spin` is in NO
72// board's graph today), and an API with no consumer has no evidence its shape
73// is the right one. The pattern it wrapped is six lines and is written out in
74// issue 0669.
75#[cfg(any(has_rmw, test))]
76pub mod monitor;
77#[cfg(any(has_rmw, test))]
78mod node;
79#[cfg(any(has_rmw, test))]
80pub mod node_record;
81#[cfg(any(has_rmw, test))]
82mod node_wake;
83// phase-436 -- one backend wake slot shared by every executor over a session.
84#[cfg(any(all(feature = "alloc", feature = "rmw-cffi"), test))]
85pub(crate) mod wake_fanout;
86// phase-397 W2 -- the per-entity wake: an arrival names its slot and owner.
87#[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
88pub(crate) mod entity_wake;
89#[cfg(any(has_rmw, test))]
90/// phase-436 W7 — installing a port's park primitive, with the wake-object
91/// constraint stated once instead of per board.
92pub mod port_park;
93// phase-359 W10 — the allocate/spawn/join helper moved to
94// `nros_platform_api::task`, beside the ABI it wraps, once `nros-cpp` became a
95// third caller. Reached through a `use` below rather than a module here.
96#[cfg(any(has_rmw, test))]
97pub(crate) mod ready_set;
98// phase-457 W3 — the per-endpoint half of `registration_path`, reported by the
99// one site that knows it. Behind a feature only a metadata probe turns on.
100#[cfg(feature = "registration-observer")]
101pub mod registration_observer;
102pub mod sched_context;
103// Issue 1971 -- the image's launch remap table, one `const` table in flash
104// that every executor reads (RFC-0107 step 1). No RMW: it is plain data.
105pub mod launch_remap;
106pub mod session_props;
107
108/// phase-509 P9b -- the executor's lock (RFC-0108 "As built in P9b").
109pub mod exec_lock;
110// phase-509 P7 (RFC-0108 D8) -- each executor's level and the image's per-level
111// liveness. Plain atomics, no RMW: every build carries it.
112pub mod level_liveness;
113pub use level_liveness::{
114 MAX_LEVELS, every_level_alive, level_count, level_last_spin_us, level_spins, set_level_count,
115 take_every_level_alive, tier_level, tier_level_by,
116};
117// phase-509 P6 (RFC-0108 D3/D7) -- the per-core scheduler and the per-level
118// worker the executor's dispatcher splits into.
119#[cfg(any(has_rmw, test))]
120pub(crate) mod scheduler;
121#[cfg(any(has_rmw, test))]
122pub(crate) mod spin;
123// Issue 2146 -- deferred dispatch for a hand-written host (RFC-0112 D2).
124#[cfg(any(has_rmw, test))]
125mod deferred;
126#[cfg(any(has_rmw, test))]
127pub(crate) mod spsc_ring;
128#[cfg(any(has_rmw, test))]
129mod storage;
130#[cfg(any(has_rmw, test))]
131pub(crate) mod triple_buffer;
132mod types;
133#[cfg(all(any(has_rmw, test), feature = "wake-latency-probe"))]
134pub mod wake_probe;
135
136#[cfg(any(has_rmw, test))]
137pub mod action;
138
139/// phase-515 R9 (decision D4) -- the sealed trait the C/C++ shims import for the
140/// executor's FFI entry points. `nros` does not re-export it.
141#[cfg(any(has_rmw, test))]
142pub mod ffi;
143
144// MockSession-based tests. Disabled when any rmw-* feature is active because
145// feature unification under `cargo test --workspace` flips `ConcreteSession`
146// to a real backend handle (e.g. UorbSession when rmw-uorb is on transitively
147// via the workspace), breaking the type signatures the tests expect.
148// `feature = "alloc"` is part of the gate, not decoration. The crate is
149// `#![no_std]`; `tests.rs` uses `std::` in 155 places (in scope because
150// `lib.rs` brings `std` in under `cfg(test)`) and calls `from_session`, which
151// is itself `#[cfg(feature = "alloc")]`. Without the
152// feature in the gate, a plain `cargo test -p nros-node` compiles this module
153// into a no_std crate and produces 252 errors, 192 of them "cannot find module
154// or crate `std`" — an incomprehensible wall for anyone running the obvious
155// command, and unrelated to whatever they were changing.
156//
157// These tests genuinely need a host: they spawn threads, sleep, and read the
158// wall clock. Gating them is not hiding coverage, it is declaring what they
159// already required. `cargo test -p nros-node --features alloc` runs them; the
160// test harness has `std` whatever the features say (phase-359 W10).
161// phase-515 R10 -- the deprecated `Executor` parameter forms keep their tests
162// for the release they remain.
163#[allow(deprecated)]
164#[cfg(all(test, feature = "alloc", not(feature = "rmw-cffi")))]
165mod tests;
166
167// phase-444 — the two `wait_for_*` decisions, in their own file rather
168// than appended to `tests.rs`. Same gate, same fixture (`MockSession`);
169// separate because what they pin is a CONTRACT choice, not executor
170// mechanics, and a reader looking for "why does Unsupported not wait?"
171// should not have to find it inside a 10 000-line file.
172#[cfg(all(test, feature = "alloc", not(feature = "rmw-cffi")))]
173mod graph_wait_tests;
174
175// Flat re-exports so users write `executor::Executor` etc.
176#[cfg(any(has_rmw, test))]
177pub use action::{
178 ActionClientRawHandle, ActionServerHandle, ActionServerRawHandle, RawActionClientSpec,
179 RawActionServerSpec,
180};
181#[cfg(any(has_rmw, test))]
182pub use action_core::{ActionClientCore, ActionServerCore, RawActiveGoal, action_channel_type};
183// phase-430 W4 — DEFINED in `crate::timer` (ungated) and re-exported here, so
184// `nros_node::executor::TimerClockSource` keeps resolving for every caller
185// phase-425 wrote while the declarative metadata layer, which builds with no
186// transport, can name the same enum instead of inventing a second one.
187pub use crate::timer::{TimerCallInfo, TimerClockSource};
188#[cfg(any(has_rmw, test))]
189pub use arena::TimerOverrunPolicy;
190#[cfg(any(has_rmw, test))]
191pub use handles::*;
192#[cfg(any(has_rmw, test))]
193// `Node` joined this line in 2026-09-12 (issue 1351). It was the only one of
194// the three that `mod node` kept to itself, and it was not a smaller surface for
195// it: `Executor::node_mut` is `pub` and RETURNS a `Node`, the `nros` crate's
196// own first doc example is `executor.node_mut(node).create_publisher::<Int32>(…)`,
197// and 15 in-tree binaries call its methods. What the missing `pub use` bought
198// was that the type had no PATH — a user could call all ~25 methods and could
199// not write `fn setup(ctx: &mut Node)` to factor two of them out, and the
200// Rust parity extractor (which walks the umbrella's reachable surface) had never
201// produced a single row for any of them.
202pub use node::{CallbackGroup, Node, NodeHandle};
203#[cfg(any(has_rmw, test))]
204pub use node_record::{NodeBuilder, NodeId, NodeRecord};
205#[cfg(any(has_rmw, test))]
206pub use spin::Executor;
207// phase-509 P9b item 2 -- a band worker over a shared executor.
208#[cfg(any(has_rmw, test))]
209pub use scheduler::{Band, BandWaitFn, BandWaiter, BandWorker, WorkerWakeFn};
210// phase-436 — the wake-source seam's types. `Executor::last_park` RETURNS a
211// `WakeSourceId` and `register_wake_source` / `set_park_primitive` TAKE the two
212// fn-pointer types, so all four are already public API; without these a caller
213// outside the crate could receive a value it cannot name or match on.
214pub use launch_remap::{BadLaunchRemap, LaunchRemap, launch_remaps, set_launch_remaps};
215pub use session_props::{
216 BadSessionProperties, MAX_SESSION_PROPERTIES, SessionProperty, lent_session_properties,
217 session_properties, set_session_properties, with_session_properties,
218};
219#[cfg(any(has_rmw, test))]
220pub use spin::SessionHandle;
221#[cfg(all(any(has_rmw, test), feature = "rmw-cffi"))]
222pub use spin::SessionSpec;
223#[cfg(any(has_rmw, test))]
224pub use spin::{
225 MAX_WAKE_SOURCES, NextDeadlineFn, PARK_EXPIRED, PARK_SIGNALLED, PARK_YIELDED, ParkUntilFn,
226 RemapRefused, WakeHookFn, WakeSourceId,
227};
228#[cfg(any(has_rmw, test))]
229pub use storage::{
230 ExecutorInlineStorage, ExecutorSizing, INLINE_BACKING_U64S, INLINE_SIZING, RegionUnit,
231 RegionUnits, executor_storage_layout, executor_storage_layout_with, executor_storage_u64_len,
232 levels_backing_bytes, levels_executor_sizing, tier_backing_bytes, tier_executor_sizing,
233 tier_sizing,
234};
235pub use types::*;