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}