nros/init.rs
1//! Phase 212.L.5 — top-level init API.
2//!
3//! Three patterns are supported (per the Phase 212.L canonical pkg shape):
4//!
5//! 1. **Node pkg** — register via the [`nros::node!`](crate::node!)
6//! macro (Phase 172 W.3); the generated runtime owns the spin loop.
7//! 2. **Application pkg + launch-aware** — call [`init_with_launch_auto`] (or
8//! [`init_with_launch`] for an explicit path). The returned [`Context`]
9//! carries launch-resolved fields (domain id, locator, RMW choice). User
10//! code drives its own spin via `Executor::open` +
11//! `Executor::spin_blocking`.
12//! 3. **Application pkg + custom spin** — call [`init()`] (or [`init_with_args`]
13//! for argv-style overrides). Launch file is ignored; env vars +
14//! `ExecutorConfig::from_env()` semantics still apply.
15//!
16//! The [`Context`] struct is a thin holder of the resolved init knobs. To
17//! actually open a session, materialise an [`crate::ExecutorConfig`] via
18//! [`Context::config`] and pass it to `Executor::open`.
19//!
20//! ## Launch overlay (current limitation)
21//!
22//! `init_with_launch_auto` / `init_with_launch` currently consume the
23//! launch-resolved knobs the parent `nros launch` process exports via env
24//! vars (`ROS_DOMAIN_ID`, `NROS_LOCATOR`, `NROS_SESSION_MODE`,
25//! `RMW_IMPLEMENTATION`, plus the placeholder `NROS_RUNTIME_OVERLAY` for
26//! the future structured overlay path). The launch XML is NOT parsed
27//! in-process; the runtime trusts the launcher to project the relevant
28//! params / remaps / env into the child environment. A follow-up wave wires
29//! the structured overlay (Option A — `nros launch --emit-runtime-overlay`
30//! → JSON sidecar consumed here). See Phase 212.L.5 notes.
31
32#[cfg(feature = "std")]
33use std::path::Path;
34
35use nros_node::ExecutorConfig;
36use nros_rmw::SessionMode;
37
38/// Errors returned by the init API.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum InitError {
41 /// `init_with_launch(path)` was passed a path that does not exist or
42 /// could not be read.
43 LaunchFileNotFound,
44 /// The launch file existed but could not be parsed.
45 ///
46 /// Phase 212.L.5 ships a stub — actual XML parsing arrives with the
47 /// runtime-overlay wave. Until then this variant is unused.
48 LaunchParseFailed,
49 /// A launch-derived env var (`ROS_DOMAIN_ID`, etc.) failed to parse.
50 EnvParseFailed,
51}
52
53impl core::fmt::Display for InitError {
54 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55 match self {
56 InitError::LaunchFileNotFound => f.write_str("launch file not found"),
57 InitError::LaunchParseFailed => f.write_str("launch file parse failed"),
58 InitError::EnvParseFailed => f.write_str("env var parse failed"),
59 }
60 }
61}
62
63#[cfg(feature = "std")]
64impl std::error::Error for InitError {}
65
66/// Phase 212.L.5 — resolved init context.
67///
68/// Returned by every `init*` entry point. Carries the fields the user
69/// needs to construct an [`ExecutorConfig`] and open a session.
70///
71/// Fields are owned (`String` on hosted builds) so the `Context` can
72/// outlive transient parents (env caches, parsed launch files).
73#[cfg(feature = "std")]
74#[derive(Debug, Clone)]
75pub struct Context {
76 /// ROS 2 domain ID (`ROS_DOMAIN_ID`, default 0).
77 pub domain_id: u32,
78 /// Middleware locator (`NROS_LOCATOR` / legacy `ZENOH_LOCATOR`).
79 pub locator: std::string::String,
80 /// Session mode (`NROS_SESSION_MODE` / legacy `ZENOH_MODE`, default `Client`).
81 pub mode: SessionMode,
82 /// RMW implementation hint (`RMW_IMPLEMENTATION` / `NROS_RMW`).
83 ///
84 /// Empty when neither var is set. The runtime uses this to pick a
85 /// primary backend when multiple are linked; see
86 /// `crate::internals::open_session`.
87 pub rmw: std::string::String,
88 /// Source of this context — useful for diagnostics + tests.
89 pub source: ContextSource,
90}
91
92/// Where the [`Context`] came from. Diagnostics only.
93#[cfg(feature = "std")]
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum ContextSource {
96 /// Built from env vars by [`init`] / [`init_with_args`].
97 Env,
98 /// Built from a launch file (path supplied to [`init_with_launch`]) or
99 /// auto-discovered via [`init_with_launch_auto`]. The launch XML itself
100 /// is NOT yet parsed (see module docs); the launcher's projected env
101 /// is the source of truth for now.
102 Launch,
103}
104
105#[cfg(feature = "std")]
106impl Context {
107 /// Materialise an [`ExecutorConfig`] for a node with the given name.
108 ///
109 /// The returned config borrows from `self`, so callers usually do:
110 ///
111 /// ```ignore
112 /// let ctx = nros::init()?;
113 /// let cfg = ctx.config("talker");
114 /// let mut executor = nros::Executor::open(&cfg)?;
115 /// ```
116 pub fn config<'a>(&'a self, node_name: &'a str) -> ExecutorConfig<'a> {
117 ExecutorConfig::new(self.locator.as_str())
118 .node_name(node_name)
119 .domain_id(self.domain_id)
120 .mode(self.mode)
121 }
122}
123
124#[cfg(feature = "std")]
125fn read_env_context(source: ContextSource) -> Result<Context, InitError> {
126 let locator = std::env::var("NROS_LOCATOR")
127 .or_else(|_| std::env::var("ZENOH_LOCATOR"))
128 .unwrap_or_else(|_| std::string::String::from("tcp/127.0.0.1:7447"));
129 let domain_id = match std::env::var("ROS_DOMAIN_ID") {
130 Ok(s) if !s.is_empty() => s.parse::<u32>().map_err(|_| InitError::EnvParseFailed)?,
131 _ => 0,
132 };
133 let mode_str = std::env::var("NROS_SESSION_MODE")
134 .or_else(|_| std::env::var("ZENOH_MODE"))
135 .unwrap_or_default();
136 let mode = match mode_str.as_str() {
137 "peer" => SessionMode::Peer,
138 _ => SessionMode::Client,
139 };
140 let rmw = std::env::var("NROS_RMW")
141 .or_else(|_| std::env::var("RMW_IMPLEMENTATION"))
142 .unwrap_or_default();
143 Ok(Context {
144 domain_id,
145 locator,
146 mode,
147 rmw,
148 source,
149 })
150}
151
152/// Pattern 3 — raw init, launch file ignored.
153///
154/// Reads env vars (`ROS_DOMAIN_ID`, `NROS_LOCATOR`, `NROS_SESSION_MODE`,
155/// `NROS_RMW` / `RMW_IMPLEMENTATION`) and returns a [`Context`]. The
156/// caller owns the spin loop — typically `Executor::open(&ctx.config(name))`
157/// followed by `spin_blocking` or a hand-rolled `spin_once` loop.
158#[cfg(feature = "std")]
159pub fn init() -> Result<Context, InitError> {
160 read_env_context(ContextSource::Env)
161}
162
163/// Pattern 3 — like [`init`] but accepts a `[--arg=value, ...]`-style argv
164/// iterator. Currently a thin wrapper over [`init`] that ignores the args;
165/// the structured argv parse (`--ros-args -p foo:=42`, etc.) lands with the
166/// runtime-overlay wave.
167#[cfg(feature = "std")]
168pub fn init_with_args<I, S>(_args: I) -> Result<Context, InitError>
169where
170 I: IntoIterator<Item = S>,
171 S: AsRef<str>,
172{
173 // TODO (Phase 212.L.5 follow-up): parse `--ros-args` style flags.
174 init()
175}
176
177/// Pattern 2 — launch-aware init.
178///
179/// Resolves the launch file via:
180///
181/// 1. `$NROS_RUNTIME_OVERLAY` — when set, the path points at a JSON sidecar
182/// written by `nros launch --emit-runtime-overlay`. (NOT yet consumed;
183/// placeholder for the follow-up wave.)
184/// 2. `<CARGO_MANIFEST_DIR>/launch/<pkg>.launch.xml` or
185/// `<CARGO_MANIFEST_DIR>/launch/system.launch.xml`. (NOT yet parsed;
186/// placeholder.)
187/// 3. The env vars described in [`init`] — the launcher projects launch
188/// params into the child env before `exec()`, so the env path is the
189/// de-facto launch overlay today.
190///
191/// Returns a [`Context`] whose `source = ContextSource::Launch` so callers
192/// can introspect whether the run is launch-driven.
193#[cfg(feature = "std")]
194pub fn init_with_launch_auto() -> Result<Context, InitError> {
195 // TODO (Phase 212.L.5 follow-up):
196 // 1. If $NROS_RUNTIME_OVERLAY is set, read the JSON sidecar and fold
197 // its params/remaps/env into the Context.
198 // 2. Else walk <CARGO_MANIFEST_DIR>/launch/* and parse the XML
199 // in-process (Option B — only if Option A overhead is rejected).
200 // For now the env path is the only overlay channel.
201 read_env_context(ContextSource::Launch)
202}
203
204/// Pattern 2 — explicit-path variant of [`init_with_launch_auto`].
205///
206/// Verifies the file exists (so misspelled paths fail fast at init time)
207/// but does NOT yet parse the XML; the launcher's projected env is the
208/// active overlay. See the module-level notes for the follow-up plan.
209#[cfg(feature = "std")]
210pub fn init_with_launch(path: impl AsRef<Path>) -> Result<Context, InitError> {
211 let p = path.as_ref();
212 if !p.exists() {
213 return Err(InitError::LaunchFileNotFound);
214 }
215 // TODO (Phase 212.L.5 follow-up): parse the launch XML and fold params
216 // / remaps / env into the returned Context. Today we only verify the
217 // file exists and fall through to the env overlay path.
218 read_env_context(ContextSource::Launch)
219}