Skip to main content

nros/
host_env.rs

1//! Issue 1763 — the boot-config ENV rung for a `#![no_std]` image that still
2//! runs inside a host process, read through the C library's `getenv`.
3//!
4//! [`crate::env`] reads the same variables through `std::env`, so it needs
5//! `std`. Until issue 1763 every image that had an environment also had `std`,
6//! so that was one fact. It stopped being one when the host simulators dropped
7//! libstd: threadx-linux's C/C++ runtime staticlib (issue 1763) and
8//! freertos-posix's (issue 1778) are `alloc`, but they are still Linux
9//! processes with a C library and an environment. Without this module those
10//! images silently stopped honouring `$ROS_DOMAIN_ID` and `$NROS_LOCATOR`.
11//! That was measured on freertos-posix: the C and C++ cells, run in parallel
12//! with distinct domains, both fell back to the baked domain and heard each
13//! other, so the C++ listener printed a `Received:` before its own talker
14//! published.
15//!
16//! The variables, their precedence and their errors are the SAME as
17//! [`crate::env`]'s. Only the reader differs. Issue 0687's rule still holds:
18//! the edge that has an environment reads it, and the core takes values. The
19//! edge is "a C library is linked", which on this tree is `target_os =
20//! "linux"`. A cross target has no `getenv` to call, and this module does not
21//! exist there.
22//!
23//! Strings are borrowed from the process environment, which outlives the boot.
24//! Nothing in nano-ros calls `setenv` after boot, and the std reader made the
25//! same assumption by caching its first read.
26
27use core::ffi::{CStr, c_char};
28
29use crate::retired_env::RETIRED_ENV_VARS;
30use nros_node::{BootConfig, BootConfigError, EnvRung, ExecutorConfig, RMW_SELECTOR_CAP};
31use nros_rmw::SessionMode;
32
33unsafe extern "C" {
34    fn getenv(name: *const c_char) -> *const c_char;
35}
36
37/// `$NAME` as a C string, or `None` when unset — the one `getenv` call.
38fn raw(name: &CStr) -> Option<&'static CStr> {
39    // SAFETY: `name` is NUL-terminated; `getenv` returns NULL or a pointer to
40    // a NUL-terminated string in the process environment, which lives for the
41    // process, so a non-NULL result is a valid `'static` C string.
42    unsafe {
43        let p = getenv(name.as_ptr());
44        (!p.is_null()).then(|| CStr::from_ptr(p))
45    }
46}
47
48/// `$NAME` as UTF-8, or `None` when unset or not UTF-8 (`std::env::var`'s
49/// `Err` cases, which the std reader also treats as unset).
50fn var(name: &CStr) -> Option<&'static str> {
51    raw(name)?.to_str().ok()
52}
53
54fn non_empty(v: Option<&'static str>) -> Option<&'static str> {
55    v.filter(|s| !s.is_empty())
56}
57
58/// The env rung, read with `getenv`. Mirrors `crate::env::env_rung`.
59fn env_rung() -> Result<EnvRung<'static>, BootConfigError> {
60    // phase-484 W8c — mirrors `crate::env::env_rung`: a set retired alias is
61    // refused (and logged, for the C / C++ shims that only see a return code).
62    for &(name, replacement) in RETIRED_ENV_VARS {
63        let mut cname = [0u8; 32];
64        cname[..name.len()].copy_from_slice(name.as_bytes());
65        let cname = CStr::from_bytes_until_nul(&cname).expect("retired env name fits");
66        // Only presence is tested, so a non-UTF-8 value still refuses.
67        if raw(cname).is_some() {
68            let err = BootConfigError::RetiredEnvVar { name, replacement };
69            nros_log::log_error!(nros_log::get_logger("nros"), "{}", err);
70            return Err(err);
71        }
72    }
73    let locator = var(c"NROS_LOCATOR");
74    let mode_str = var(c"NROS_SESSION_MODE");
75    let mode = match mode_str {
76        Some("peer") => SessionMode::Peer,
77        _ => SessionMode::Client,
78    };
79    let domain_id = match non_empty(var(c"ROS_DOMAIN_ID")) {
80        Some(s) => Some(
81            s.trim()
82                .parse::<u32>()
83                .map_err(|_| BootConfigError::DomainIdParse)?,
84        ),
85        None => None,
86    };
87    let rmw = non_empty(var(c"NROS_RMW")).filter(|s| s.len() <= RMW_SELECTOR_CAP);
88    Ok(EnvRung {
89        locator,
90        domain_id,
91        mode: Some(mode),
92        node_name: non_empty(var(c"NROS_NODE_NAME")),
93        namespace: non_empty(var(c"NROS_NODE_NAMESPACE")),
94        rmw,
95    })
96}
97
98/// [`ExecutorConfig::try_resolve_with`] with the `getenv` rung — the
99/// `#![no_std]` hosted twin of [`crate::env::try_resolve_hosted`].
100pub fn try_resolve_hosted<'a>(
101    baked: BootConfig<'a>,
102) -> Result<ExecutorConfig<'a>, BootConfigError> {
103    ExecutorConfig::try_resolve_with(baked, Some(env_rung()?))
104}