Skip to main content

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}