Skip to main content

nros_node/executor/
session_props.rs

1//! A bringup's own RMW session configuration, baked into the image as ONE
2//! const table -- RFC-0109 D8, phase-514 W3.
3//!
4//! The user writes their middleware's own file (`rmw/zenoh.json5`, the
5//! document `rmw_zenoh` reads from `ZENOH_SESSION_CONFIG_URI`). The build
6//! lowers it ON THE HOST into the backend's session properties
7//! (`nros-zenoh-session-config`), and the generated entry hands the result to
8//! the runtime once, before the board runner opens any session
9//! ([`set_session_properties`], `nros_cpp_set_session_properties`) -- the same
10//! shape as the launch remap table ([`super::launch_remap`]). Every
11//! [`crate::Executor::open`] that is not given properties of its own passes
12//! these to the backend as `RmwConfig::properties`.
13//!
14//! The table describes the image's DEFAULT session. A second session
15//! ([`crate::SessionSpec`], a bridge's other domain) is not given it.
16
17use core::ffi::{CStr, c_char};
18
19use portable_atomic::{AtomicPtr, AtomicUsize, Ordering};
20
21/// One session property, as the generated entry emits it: two NUL-terminated
22/// strings in `.rodata`. `#[repr(C)]` so the C table
23/// (`nros_cpp_session_property_t`) is this struct.
24#[repr(C)]
25#[derive(Clone, Copy, Debug)]
26pub struct SessionProperty {
27    /// The backend's property name (`listen`, `connect`, `mode`, ...).
28    pub key: *const c_char,
29    /// Its value.
30    pub value: *const c_char,
31}
32
33// SAFETY: a `SessionProperty` only ever points at `'static`, immutable,
34// NUL-terminated strings (the constructor below, and the C ABI's contract on
35// `nros_cpp_set_session_properties`); nothing writes through the pointers.
36unsafe impl Sync for SessionProperty {}
37// SAFETY: as above.
38unsafe impl Send for SessionProperty {}
39
40impl SessionProperty {
41    /// A property over `'static` C strings -- the Rust spelling of one row of
42    /// the generated table.
43    pub const fn new(key: &'static CStr, value: &'static CStr) -> Self {
44        Self {
45            key: key.as_ptr(),
46            value: value.as_ptr(),
47        }
48    }
49
50    /// Both fields as `str`, or `None` when one is NULL or not UTF-8 --
51    /// [`set_session_properties`] refuses such a table, so a stored row reads.
52    pub fn strs(&self) -> Option<(&'static str, &'static str)> {
53        let s = |p: *const c_char| -> Option<&'static str> {
54            if p.is_null() {
55                None
56            } else {
57                // SAFETY: the type's invariant -- every non-NULL pointer is a
58                // `'static` NUL-terminated string.
59                unsafe { CStr::from_ptr(p) }.to_str().ok()
60            }
61        };
62        Some((s(self.key)?, s(self.value)?))
63    }
64}
65
66/// Why [`set_session_properties`] refused a table.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum BadSessionProperties {
69    /// The row at this index has a NULL or non-UTF-8 string.
70    Row(usize),
71    /// More rows than one session open can carry ([`MAX_SESSION_PROPERTIES`]).
72    TooMany(usize),
73}
74
75impl core::fmt::Display for BadSessionProperties {
76    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
77        match self {
78            Self::Row(i) => write!(
79                f,
80                "session property row {i} has a NULL key or value, or a string that is not UTF-8"
81            ),
82            Self::TooMany(n) => write!(
83                f,
84                "{n} session properties; one session open carries at most {MAX_SESSION_PROPERTIES}"
85            ),
86        }
87    }
88}
89
90/// The most rows one session open hands the backend. The zenoh shim's own
91/// cap is lower on an embedded build and refuses a longer list there, naming
92/// it; this one bounds the stack copy [`with_session_properties`] makes.
93pub const MAX_SESSION_PROPERTIES: usize = 16;
94
95static TABLE: AtomicPtr<SessionProperty> = AtomicPtr::new(core::ptr::null_mut());
96static LEN: AtomicUsize = AtomicUsize::new(0);
97
98/// Install the image's baked session properties. Call it ONCE, at boot,
99/// before any executor opens a session -- the generated entry calls it before
100/// the board runner. A row that does not read refuses the whole table and
101/// installs nothing.
102pub fn set_session_properties(
103    table: &'static [SessionProperty],
104) -> Result<(), BadSessionProperties> {
105    if table.len() > MAX_SESSION_PROPERTIES {
106        return Err(BadSessionProperties::TooMany(table.len()));
107    }
108    if let Some(i) = table.iter().position(|r| r.strs().is_none()) {
109        return Err(BadSessionProperties::Row(i));
110    }
111    // Boot-only, single writer: hide the old table, swap the pointer, then
112    // publish the length, so a reader never pairs a pointer with a longer
113    // length than its table.
114    LEN.store(0, Ordering::Release);
115    TABLE.store(table.as_ptr() as *mut SessionProperty, Ordering::Release);
116    LEN.store(table.len(), Ordering::Release);
117    Ok(())
118}
119
120/// The installed table (empty until [`set_session_properties`]).
121pub fn session_properties() -> &'static [SessionProperty] {
122    let len = LEN.load(Ordering::Acquire);
123    let ptr = TABLE.load(Ordering::Acquire);
124    if len == 0 || ptr.is_null() {
125        return &[];
126    }
127    // SAFETY: `ptr`/`len` came from one `&'static [SessionProperty]` in
128    // `set_session_properties` (the length is published after the pointer).
129    unsafe { core::slice::from_raw_parts(ptr, len) }
130}
131
132/// Run `f` over the properties a session open hands its backend: `explicit`
133/// when the caller supplied any, else the installed table. Every road's open
134/// goes through here (`Executor::open`, the C API's `open_session`), so the
135/// table cannot reach one road and miss another.
136pub fn with_session_properties<R>(
137    explicit: &[(&str, &str)],
138    f: impl FnOnce(&[(&str, &str)]) -> R,
139) -> R {
140    if !explicit.is_empty() {
141        return f(explicit);
142    }
143    let mut rows: heapless::Vec<(&'static str, &'static str), MAX_SESSION_PROPERTIES> =
144        heapless::Vec::new();
145    for r in session_properties() {
146        // `set_session_properties` checked every row reads and the count fits.
147        if let Some(kv) = r.strs() {
148            let _ = rows.push(kv);
149        }
150    }
151    f(&rows)
152}
153
154/// Read a table a caller LENDS for one multi-session open (issue 2123): the
155/// C/C++ spelling of [`crate::SessionSpec::properties`], `rows`/`len` straight
156/// off `NrosCppSessionSpec` or `rclc_session_spec_t`. Writes the rows into the
157/// front of `out` and returns how many; `rows` NULL with `len == 0` is no
158/// table. Refuses (writing nothing usable) a NULL `rows` with `len > 0` or a
159/// row that does not read ([`BadSessionProperties::Row`]), and more rows than
160/// one session open carries or than `out` holds ([`BadSessionProperties::TooMany`]).
161///
162/// Unlike [`set_session_properties`] the table need not be `'static`: a
163/// session open hands the properties to its backend and keeps none of them.
164///
165/// # Safety
166/// `rows` must point at `len` rows that, with every string they point at, are
167/// valid and unwritten for `'a`.
168pub unsafe fn lent_session_properties<'a>(
169    rows: *const SessionProperty,
170    len: usize,
171    out: &mut [(&'a str, &'a str)],
172) -> Result<usize, BadSessionProperties> {
173    if len == 0 {
174        return Ok(0);
175    }
176    if rows.is_null() {
177        return Err(BadSessionProperties::Row(0));
178    }
179    if len > MAX_SESSION_PROPERTIES || len > out.len() {
180        return Err(BadSessionProperties::TooMany(len));
181    }
182    // SAFETY: the caller's contract.
183    let rows = unsafe { core::slice::from_raw_parts(rows, len) };
184    for (i, (r, slot)) in rows.iter().zip(out.iter_mut()).enumerate() {
185        let s = |p: *const c_char| -> Option<&'a str> {
186            if p.is_null() {
187                None
188            } else {
189                // SAFETY: the caller's contract -- non-NULL, NUL-terminated,
190                // live for `'a`.
191                unsafe { CStr::from_ptr(p) }.to_str().ok()
192            }
193        };
194        *slot = (
195            s(r.key).ok_or(BadSessionProperties::Row(i))?,
196            s(r.value).ok_or(BadSessionProperties::Row(i))?,
197        );
198    }
199    Ok(len)
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use std::string::{String, ToString};
206
207    #[test]
208    fn a_lent_table_reads_into_the_callers_buffer_and_refuses_what_does_not_read() {
209        let rows = [
210            SessionProperty::new(c"mode", c"client"),
211            SessionProperty::new(c"connect", c"tcp/10.0.0.1:7447"),
212        ];
213        let mut out = [("", ""); 4];
214        let n = unsafe { lent_session_properties(rows.as_ptr(), rows.len(), &mut out) };
215        assert_eq!(n, Ok(2));
216        assert_eq!(
217            out[..2],
218            [("mode", "client"), ("connect", "tcp/10.0.0.1:7447")]
219        );
220
221        assert_eq!(
222            unsafe { lent_session_properties(core::ptr::null(), 0, &mut out) },
223            Ok(0),
224            "NULL with no rows is no table"
225        );
226        assert_eq!(
227            unsafe { lent_session_properties(core::ptr::null(), 1, &mut out) },
228            Err(BadSessionProperties::Row(0))
229        );
230        let bad = [
231            SessionProperty::new(c"mode", c"client"),
232            SessionProperty {
233                key: c"connect".as_ptr(),
234                value: core::ptr::null(),
235            },
236        ];
237        assert_eq!(
238            unsafe { lent_session_properties(bad.as_ptr(), bad.len(), &mut out) },
239            Err(BadSessionProperties::Row(1))
240        );
241        let mut one = [("", ""); 1];
242        assert_eq!(
243            unsafe { lent_session_properties(rows.as_ptr(), rows.len(), &mut one) },
244            Err(BadSessionProperties::TooMany(2)),
245            "more rows than the buffer holds is refused, never truncated"
246        );
247        let many = [SessionProperty::new(c"k", c"v"); MAX_SESSION_PROPERTIES + 1];
248        let mut big = [("", ""); MAX_SESSION_PROPERTIES + 1];
249        assert_eq!(
250            unsafe { lent_session_properties(many.as_ptr(), many.len(), &mut big) },
251            Err(BadSessionProperties::TooMany(MAX_SESSION_PROPERTIES + 1))
252        );
253    }
254
255    #[test]
256    fn a_table_installs_and_an_explicit_list_wins() {
257        static T: [SessionProperty; 2] = [
258            SessionProperty::new(c"mode", c"client"),
259            SessionProperty::new(c"connect", c"tcp/10.0.2.2:7447"),
260        ];
261        set_session_properties(&T).unwrap();
262        let got = with_session_properties(&[], |p| {
263            p.iter()
264                .map(|(k, v)| (k.to_string(), v.to_string()))
265                .collect::<std::vec::Vec<_>>()
266        });
267        let want: [(String, String); 2] = [
268            ("mode".into(), "client".into()),
269            ("connect".into(), "tcp/10.0.2.2:7447".into()),
270        ];
271        assert_eq!(got, want);
272        let explicit = [("listen", "tcp/0.0.0.0:1")];
273        assert_eq!(with_session_properties(&explicit, |p| p.len()), 1);
274
275        static BAD: [SessionProperty; 1] = [SessionProperty {
276            key: c"mode".as_ptr(),
277            value: core::ptr::null(),
278        }];
279        assert_eq!(
280            set_session_properties(&BAD),
281            Err(BadSessionProperties::Row(0))
282        );
283        // A refused table leaves the installed one in place.
284        assert_eq!(session_properties().len(), 2);
285    }
286}