Skip to main content

nros_node/executor/
launch_remap.rs

1//! Launch remap rules as ONE const table in flash -- issue 1971, RFC-0107
2//! step 1.
3//!
4//! The launch file's `<remap from= to=/>` rules describe the IMAGE, not one
5//! executor. Before this every executor carved a RAM table of 16 rules
6//! (`RemapRule`, 404 B each on thumbv7em: 6,464 B per backing) and the
7//! generated C/C++ entry filled it with one `nros_cpp_declare_remap` call per
8//! rule, on every tier's executor. Now codegen emits the rules once, as a
9//! `static const` array of string pointers, and hands it to the runtime once
10//! ([`set_launch_remaps`], `nros_cpp_set_launch_remaps`) before any tier is
11//! set up. Every executor reads that table; none copies it.
12//!
13//! Layering is unchanged: these rules are the AUTHORITATIVE tier of
14//! [`crate::names::resolve_name_layered`], and `--ros-args -r` rules (still in
15//! the executor's RAM table, see `ExecutorSizing::remaps`) are the fallback.
16//!
17//! The Rust entry already worked this way: its launch remaps are a `&'static`
18//! slice on `RuntimeCtx`, and this table is the C/C++ twin of it.
19
20use core::ffi::{CStr, c_char};
21
22use portable_atomic::{AtomicPtr, AtomicUsize, Ordering};
23
24/// One launch remap rule, in the shape the generated C/C++ entry emits:
25/// four NUL-terminated strings in `.rodata`.
26///
27/// `#[repr(C)]` so the C table (`nros_cpp_launch_remap_t`) is this struct.
28/// `namespace` NULL or empty means the root namespace, as `declare_remap`
29/// read it.
30#[repr(C)]
31#[derive(Clone, Copy, Debug)]
32pub struct LaunchRemap {
33    /// The owning node's name.
34    pub node_name: *const c_char,
35    /// The owning node's namespace (`/` for root; NULL or empty also root).
36    pub namespace: *const c_char,
37    /// The name as written in the source (raw; expanded at lookup).
38    pub from: *const c_char,
39    /// The replacement (raw; expanded at lookup).
40    pub to: *const c_char,
41}
42
43// SAFETY: a `LaunchRemap` only ever points at `'static`, immutable,
44// NUL-terminated strings (the constructors below, and the C ABI's contract on
45// `nros_cpp_set_launch_remaps`); nothing writes through the pointers.
46unsafe impl Sync for LaunchRemap {}
47// SAFETY: as above.
48unsafe impl Send for LaunchRemap {}
49
50impl LaunchRemap {
51    /// A rule over `'static` C strings -- the Rust spelling of one row of the
52    /// generated table.
53    pub const fn new(
54        node_name: &'static CStr,
55        namespace: &'static CStr,
56        from: &'static CStr,
57        to: &'static CStr,
58    ) -> Self {
59        Self {
60            node_name: node_name.as_ptr(),
61            namespace: namespace.as_ptr(),
62            from: from.as_ptr(),
63            to: to.as_ptr(),
64        }
65    }
66
67    /// The four fields as `str`, or `None` when one is NULL (namespace
68    /// excepted) or not UTF-8 -- [`set_launch_remaps`] refuses such a table,
69    /// so a stored rule always reads.
70    fn strs(&self) -> Option<(&'static str, &'static str, &'static str, &'static str)> {
71        // SAFETY: the type's invariant -- every non-NULL pointer is a
72        // `'static` NUL-terminated string.
73        let s = |p: *const c_char| -> Option<&'static str> {
74            if p.is_null() {
75                None
76            } else {
77                unsafe { CStr::from_ptr(p) }.to_str().ok()
78            }
79        };
80        let ns = if self.namespace.is_null() {
81            "/"
82        } else {
83            match s(self.namespace)? {
84                "" => "/",
85                ns => ns,
86            }
87        };
88        Some((s(self.node_name)?, ns, s(self.from)?, s(self.to)?))
89    }
90}
91
92/// Why [`set_launch_remaps`] refused a table.
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub struct BadLaunchRemap {
95    /// The row's index in the table.
96    pub index: usize,
97}
98
99impl core::fmt::Display for BadLaunchRemap {
100    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
101        write!(
102            f,
103            "launch remap row {} has a NULL node name, `from` or `to`, or a string \
104             that is not UTF-8",
105            self.index
106        )
107    }
108}
109
110static TABLE: AtomicPtr<LaunchRemap> = AtomicPtr::new(core::ptr::null_mut());
111static LEN: AtomicUsize = AtomicUsize::new(0);
112
113/// Install the image's launch remap table. Every executor reads it from then
114/// on, on every road (C and C++ registration, `Executor::create_node`'s
115/// handle).
116///
117/// Call it ONCE, at boot, before any executor registers an entity -- the
118/// generated entry calls it before the board runner sets up any tier, because
119/// an rclcpp-shape constructor registers its entities immediately (issue
120/// 0255). A row that does not read (NULL or non-UTF-8 string) refuses the
121/// whole table and installs nothing.
122pub fn set_launch_remaps(table: &'static [LaunchRemap]) -> Result<(), BadLaunchRemap> {
123    if let Some(index) = table.iter().position(|r| r.strs().is_none()) {
124        return Err(BadLaunchRemap { index });
125    }
126    // Boot-only, single writer: hide the old table, swap the pointer, then
127    // publish the length, so a reader never pairs a pointer with a longer
128    // length than its table.
129    LEN.store(0, Ordering::Release);
130    TABLE.store(table.as_ptr() as *mut LaunchRemap, Ordering::Release);
131    LEN.store(table.len(), Ordering::Release);
132    Ok(())
133}
134
135/// The installed launch remap table (empty until [`set_launch_remaps`]).
136pub fn launch_remaps() -> &'static [LaunchRemap] {
137    let len = LEN.load(Ordering::Acquire);
138    let ptr = TABLE.load(Ordering::Acquire);
139    if len == 0 || ptr.is_null() {
140        return &[];
141    }
142    // SAFETY: `ptr`/`len` came from one `&'static [LaunchRemap]` in
143    // `set_launch_remaps` (the length is published after the pointer).
144    unsafe { core::slice::from_raw_parts(ptr, len) }
145}
146
147/// The rules of `table` that belong to the node `(node_name, namespace)`, in
148/// table order, as the `(from, to)` pairs `resolve_name_layered` takes.
149#[cfg_attr(not(any(has_rmw, test)), allow(dead_code))]
150pub(crate) fn launch_rules_for<'r>(
151    table: &'r [LaunchRemap],
152    node_name: &'r str,
153    namespace: &'r str,
154) -> impl Iterator<Item = (&'r str, &'r str)> + 'r {
155    let ns = if namespace.is_empty() { "/" } else { namespace };
156    table.iter().filter_map(move |r| {
157        let (n, rns, from, to) = r.strs()?;
158        (n == node_name && rns == ns).then_some((from, to))
159    })
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    static TABLE: [LaunchRemap; 3] = [
167        LaunchRemap::new(c"talker", c"/", c"chatter", c"/wire"),
168        LaunchRemap::new(c"talker", c"/ns", c"chatter", c"/other_ns"),
169        LaunchRemap::new(c"listener", c"", c"chatter", c"/heard"),
170    ];
171
172    #[test]
173    fn rows_are_selected_by_node_name_and_namespace() {
174        let got: std::vec::Vec<_> = launch_rules_for(&TABLE, "talker", "/").collect();
175        assert_eq!(got, [("chatter", "/wire")]);
176        let got: std::vec::Vec<_> = launch_rules_for(&TABLE, "talker", "/ns").collect();
177        assert_eq!(got, [("chatter", "/other_ns")]);
178    }
179
180    #[test]
181    fn an_empty_namespace_is_the_root_on_both_sides() {
182        let got: std::vec::Vec<_> = launch_rules_for(&TABLE, "listener", "").collect();
183        assert_eq!(got, [("chatter", "/heard")]);
184        let got: std::vec::Vec<_> = launch_rules_for(&TABLE, "listener", "/").collect();
185        assert_eq!(got, [("chatter", "/heard")]);
186    }
187
188    #[test]
189    fn a_row_with_a_null_string_refuses_the_whole_table() {
190        static BAD: [LaunchRemap; 2] = [
191            LaunchRemap::new(c"talker", c"/", c"a", c"/b"),
192            LaunchRemap {
193                node_name: core::ptr::null(),
194                namespace: core::ptr::null(),
195                from: c"a".as_ptr(),
196                to: c"/b".as_ptr(),
197            },
198        ];
199        assert_eq!(set_launch_remaps(&BAD), Err(BadLaunchRemap { index: 1 }));
200    }
201}