Skip to main content

nros_platform/board/
runtime.rs

1//! [`RuntimeCtx`] — Phase 212.N.1.
2//!
3//! Per-invocation runtime context handed to `BoardEntry::run`'s
4//! `setup` callback. Carries the overlay knobs the codegen
5//! `run_plan(runtime)` body reads:
6//!
7//! - **params** — `(key, value)` pairs from launch XML
8//!   `<param name="…" value="…"/>` or `--ros-args -p k:=v`.
9//! - **remaps** — `(from, to)` topic/service renames.
10//! - **env** — environment-style key/value pairs (POSIX `getenv`
11//!   shape) accessible from no_std boards via this struct rather
12//!   than a `libc::getenv` call.
13//! - **runtime** — `&mut dyn NodeDispatchRuntime` sink the
14//!   codegen-emitted `run_plan(runtime)` body forwards each Node
15//!   pkg's `register(runtime)` call into (Phase 212.N.7 step-3.2).
16//!   Populated by each `BoardEntry::run` impl after opening its
17//!   executor; defaults to a no-op sink when constructed via
18//!   [`RuntimeCtx::with_runtime`].
19//!
20//! ## no_std-safe shape
21//!
22//! Slice-of-tuples kept on the boot stack. No allocation, no
23//! `core::collections`. Codegen owns the storage and passes a
24//! `&mut RuntimeCtx<'_>` whose backing slices live in `static`s.
25//!
26//! Hosted boards (POSIX) may instead build a longer-lived owned
27//! variant on the heap; the trait surface is slice-based so
28//! both shapes work.
29
30use super::dispatch::DispatchStrategy;
31
32/// Layer-clean substitute for `nros::node_metadata::CallbackId` +
33/// `nros::node::CallbackCtx` at the [`NodeDispatchRuntime`] boundary
34/// (Phase 216.A.2). `nros-platform` sits below `nros` in the dep
35/// graph, so the trait surface cannot reference those types
36/// directly. The `nros`-side runtime impl wraps a real
37/// `(CallbackId, &mut CallbackCtx)` pair into this opaque shape
38/// before invoking [`NodeDispatchRuntime::signal_callback`]; the
39/// concrete dispatcher casts `ctx_ptr` back to
40/// `&mut nros::CallbackCtx<'_>` at the call site.
41///
42/// `#[repr(C)]` keeps the layout stable across the (same-language
43/// today, FFI-shaped tomorrow) `nros-platform` ↔ `nros` boundary.
44#[repr(C)]
45pub struct SignaledCallback<'a> {
46    /// Stable identifier string carried by `nros::CallbackId(&'a str)`.
47    pub cb_id: &'a str,
48
49    /// Erased pointer to the `nros::CallbackCtx<'_>` the dispatcher
50    /// will drive. The `nros`-side `NodeDispatchRuntime` impl casts
51    /// back to `&mut nros::CallbackCtx<'_>` before invoking the
52    /// component body.
53    pub ctx_ptr: *mut core::ffi::c_void,
54}
55
56// Phase 258 (Track 2, w5) — the opaque per-Node `extern "Rust" fn()` aliases
57// (`NodeRegisterFn` / `NodeInitFn` / `NodeDispatchFn` / `NodeTickFn`) are gone.
58// They anchored the retired `register_dispatch_slot_dyn` four-fn-ptr bridge
59// (owned-spin declarative register), which the install seam replaced — Rust
60// owned-spin now registers via `RuntimeCtx::runtime.executor_handle()` +
61// `nros::install_node_typed` like the C/C++ typed entries.
62
63/// Node runtime sink the codegen-emitted `run_plan(runtime)`
64/// body talks to (Phase 212.N.7 step-3.1).
65///
66/// Object-safe + `no_std`. The concrete impl
67/// (`ExecutorNodeRuntime` in `nros`) owns the live executor;
68/// `BoardEntry::run` installs it on the per-boot
69/// [`RuntimeCtx::runtime`] slot before invoking the user `setup`
70/// closure.
71///
72/// Phase 214.K.1 — renamed from `NodeRuntime` to disambiguate from
73/// the user-facing `nros::NodeRuntime` metadata-sink trait in
74/// `packages/core/nros/src/node.rs:112`. The two traits live at
75/// different layers (board-side dispatch sink vs user-side metadata
76/// declaration sink) and the previous shared name forced explicit
77/// `nros_platform::` / `nros::` qualification at every use site +
78/// produced confusing `impl NodeRuntime for X` ambiguity. A
79/// `#[deprecated]` `pub use NodeDispatchRuntime as NodeRuntime;`
80/// re-export sits at the crate module level for one release cycle.
81pub trait NodeDispatchRuntime {
82    /// Drive the underlying executor for at most `timeout_ms`
83    /// milliseconds. `Ok(())` on a clean spin (including timeout);
84    /// `Err(())` if the executor surfaces a spin error.
85    ///
86    /// `Result<_, ()>` is deliberate: the board entry-point callers
87    /// (`nros-board-{freertos,nuttx,threadx}` spin loops) only
88    /// `{:?}`-print the error and `B::exit_failure()` — a typed enum
89    /// would carry no extra info across the trait boundary, since the
90    /// underlying `ExecutorError` from `nros::node_runtime` is mapped
91    /// to `()` at the impl site (`impl NodeDispatchRuntime for
92    /// ExecutorNodeRuntime`). `#[allow]` keeps the surface narrow.
93    #[allow(clippy::result_unit_err)]
94    fn spin_once(&mut self, timeout_ms: u32) -> Result<(), ()>;
95
96    /// Phase 258 (Track 2, 2a) — raw `*mut Executor` (as `void*`) for the
97    /// owned-spin entry, so a Node pkg's `register(runtime)` wrapper can call
98    /// the uniform `__nros_component_<pkg>_install(.., executor, ..)` seam
99    /// (`nros::install_node_typed`) instead of the retired opaque-fn-ptr
100    /// `register_dispatch_slot_dyn` bridge. A pointer crosses the
101    /// `nros-platform` → `nros` layering wall cleanly (the concrete
102    /// `ExecutorNodeRuntime` lives in `nros`; this trait can't name it).
103    ///
104    /// Default `null` — sinks without a live executor (e.g.
105    /// [`NullNodeRuntime`], framework-dispatch-only runtimes) report no
106    /// handle; the install path treats null as a registration error.
107    fn executor_handle(&mut self) -> *mut core::ffi::c_void {
108        core::ptr::null_mut()
109    }
110
111    /// Observability counters from hosted/runtime tests.
112    ///
113    /// Returns `(all_callbacks, message_callbacks)`. Implementations
114    /// that cannot observe callback dispatch keep the default zeros.
115    fn observed_callback_counts(&self) -> (usize, usize) {
116        (0, 0)
117    }
118
119    /// Hand a signaled callback to the framework-side dispatcher
120    /// (Phase 216.A.2). Only meaningful for `DispatchStrategy::Deferred`
121    /// (RTIC / Embassy) runtimes — `Inline` runtimes drive callbacks
122    /// directly from `spin_once` and never call this. The default panic
123    /// surfaces the mis-wire loudly rather than silently dropping the
124    /// callback signal.
125    fn signal_callback(&mut self, _cb: SignaledCallback<'_>) {
126        panic!("signal_callback not implemented for Inline runtime");
127    }
128
129    /// Declare how this runtime delivers callbacks (Phase 216.A.2).
130    /// `nros check` (Phase 216.D.1) cross-validates each Node pkg's
131    /// `Node::DISPATCH` against this value. Defaults to `Inline` so
132    /// every existing impl reports the historical behavior unchanged.
133    fn dispatch_strategy(&self) -> DispatchStrategy {
134        DispatchStrategy::Inline
135    }
136}
137
138/// No-op [`NodeDispatchRuntime`] for tests / placeholders. Every call
139/// returns `Err(())` so callers that depend on a populated runtime
140/// fail loud rather than silently no-op.
141///
142/// `BoardEntry::run` impls replace this with a real
143/// `ExecutorNodeRuntime`-backed sink before invoking the user
144/// `setup` closure.
145#[derive(Debug, Default)]
146pub struct NullNodeRuntime;
147
148impl NodeDispatchRuntime for NullNodeRuntime {
149    fn spin_once(&mut self, _timeout_ms: u32) -> Result<(), ()> {
150        Err(())
151    }
152}
153
154/// Runtime context handed to `BoardEntry::run(setup)`.
155///
156/// All three overlay slices may be empty. A board's launch overlay
157/// typically populates `params` + `remaps`; `env` is rarely set on
158/// embedded.
159pub struct RuntimeCtx<'a> {
160    /// `<param name=… value=…/>` from launch XML, or
161    /// `-p name:=value` CLI overrides.
162    pub params: &'a [(&'a str, &'a str)],
163
164    /// Topic / service / action remaps: `(from, to)`.
165    pub remaps: &'a [(&'a str, &'a str)],
166
167    /// Environment-style key/value pairs (mostly POSIX). Empty on
168    /// embedded boards.
169    pub env: &'a [(&'a str, &'a str)],
170
171    /// Node runtime sink. `BoardEntry::run` populates this with
172    /// the live `ExecutorNodeRuntime`-backed impl before invoking
173    /// the user `setup` closure. The codegen-emitted
174    /// `run_plan(runtime)` body calls `<pkg>::register(runtime)` once per Node
175    /// pkg, which installs through `runtime.executor_handle()` +
176    /// `nros::install_node_typed` (Phase 258, Track 2).
177    ///
178    /// Defaults to a [`NullNodeRuntime`] when the context is
179    /// built via [`RuntimeCtx::with_runtime`]. That sink errors
180    /// every call so test fixtures that forget to wire a real runtime
181    /// fail loud.
182    pub runtime: &'a mut dyn NodeDispatchRuntime,
183}
184
185impl core::fmt::Debug for RuntimeCtx<'_> {
186    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
187        f.debug_struct("RuntimeCtx")
188            .field("params", &self.params)
189            .field("remaps", &self.remaps)
190            .field("env", &self.env)
191            .field("runtime", &"<dyn NodeDispatchRuntime>")
192            .finish()
193    }
194}
195
196impl<'a> RuntimeCtx<'a> {
197    /// Build a [`RuntimeCtx`] with no params / remaps / env and the
198    /// given runtime sink. The common shape `BoardEntry::run`
199    /// constructs after opening its executor.
200    ///
201    /// For test fixtures that don't need a populated runtime, pass a
202    /// `&mut NullNodeRuntime` — every call against the sink
203    /// returns `Err(())`, surfacing the missing wiring.
204    pub fn with_runtime(runtime: &'a mut dyn NodeDispatchRuntime) -> Self {
205        Self {
206            params: &[],
207            remaps: &[],
208            env: &[],
209            runtime,
210        }
211    }
212
213    /// Build a [`RuntimeCtx`] with explicit overlay slices + runtime
214    /// sink (Phase 212.N.7 step-3.2).
215    pub fn new(
216        runtime: &'a mut dyn NodeDispatchRuntime,
217        params: &'a [(&'a str, &'a str)],
218        remaps: &'a [(&'a str, &'a str)],
219        env: &'a [(&'a str, &'a str)],
220    ) -> Self {
221        Self {
222            params,
223            remaps,
224            env,
225            runtime,
226        }
227    }
228
229    /// Lookup a param by name; first match wins. Linear scan
230    /// because the slice is typically small (≤ a dozen entries).
231    pub fn param(&self, name: &str) -> Option<&'a str> {
232        self.params
233            .iter()
234            .find(|(k, _)| *k == name)
235            .map(|(_, v)| *v)
236    }
237
238    /// Lookup a remap by the original (`from`) name; returns the
239    /// rewritten name when remapped, else `None`.
240    pub fn remap(&self, from: &str) -> Option<&'a str> {
241        self.remaps
242            .iter()
243            .find(|(k, _)| *k == from)
244            .map(|(_, v)| *v)
245    }
246
247    /// Lookup an env entry by name.
248    pub fn env_var(&self, name: &str) -> Option<&'a str> {
249        self.env.iter().find(|(k, _)| *k == name).map(|(_, v)| *v)
250    }
251}
252
253/// Error returned by the codegen-emitted `run_plan(runtime)` body
254/// (Phase 212.N.4) and by Node pkg `register(runtime)` wrappers
255/// (Phase 212.N.7 step-2).
256///
257/// `no_std`-safe — variants are string-typed so embedded Entry pkgs
258/// don't need to pull `thiserror`/`anyhow` to print. The
259/// out-of-tree `nros-build` codegen library re-exports this type so
260/// emitted code references `::nros_platform::RuntimeError`, NOT
261/// `::nros_build::RuntimeError` — the embedded Entry pkg's runtime
262/// path then doesn't need `nros-build` as a runtime dep (build-dep
263/// only).
264#[derive(Debug)]
265#[non_exhaustive]
266pub enum RuntimeError {
267    /// A node's `register(runtime)` call failed. The string carries the
268    /// node pkg name.
269    ///
270    /// Phase 212.N.12 hard-renamed the legacy `ComponentRegister` variant
271    /// to `NodeRegister` to match the rclcpp_components / ROS 2 launch.xml
272    /// `<node pkg=…>` convention.
273    NodeRegister(&'static str),
274
275    /// The hosted Entry spin loop failed or did not observe the
276    /// requested runtime condition before its bounded test deadline.
277    Spin,
278}
279
280impl core::fmt::Display for RuntimeError {
281    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
282        match self {
283            Self::NodeRegister(msg) => write!(f, "node register failed: {msg}"),
284            Self::Spin => write!(f, "entry spin failed"),
285        }
286    }
287}