Skip to main content

nros_log/
pool.rs

1//! phase-417 W4.d — bounded storage so `get_or_create_logger` can hand out a
2//! logger for a name nobody registered at compile time.
3//!
4//! ## The gap this closes
5//!
6//! [`crate::get_logger`] is a LOOKUP: it answers with
7//! [`crate::DEFAULT_LOGGER`] for any name no `'static` [`crate::Logger`] was
8//! [`crate::register_logger`]ed under. That is correct for the Rust call sites
9//! it was written for, which declare their logger as a `static`. It is the
10//! wrong answer for a wrapper: `rclcpp::get_logger("x")` and
11//! `rcutils_get_logger("x")` CREATE, so a C caller doing
12//!
13//! ```c
14//! rclc_logger_set_level(rclc_log_get_logger("nav"), RCLC_LOG_SEVERITY_DEBUG);
15//! ```
16//!
17//! over a lookup-only implementation would silently have moved the threshold
18//! of the catch-all logger every other unregistered name also resolves to —
19//! compile, and differ. RFC-0089 forbids exactly that, so the creation has to
20//! be real, and by RFC-0019 it has to be real HERE rather than in `nros-c`.
21//!
22//! ## Bounded, because `no_std`
23//!
24//! Two static arenas, both sized by the `NROS_LOG_DYNAMIC_LOGGERS` knob
25//! (phase-479 W5, RFC-0102 D5 — see `build.rs` for its ladder):
26//! one for the [`crate::Logger`] values, one for their names (a `&'static str`
27//! is what `Logger` holds, and a `const char *` from C is not one). Exhaustion
28//! is REPORTED, never silently aliased onto the default logger — see
29//! [`crate::get_or_create_logger`].
30
31use core::{cell::UnsafeCell, mem::MaybeUninit};
32
33use portable_atomic::{AtomicUsize, Ordering};
34
35use crate::Logger;
36
37/// How many loggers [`crate::get_or_create_logger`] may create.
38///
39/// The `NROS_LOG_DYNAMIC_LOGGERS` knob, resolved once by `build.rs` on the
40/// RFC-0049 ladder: the environment (which is where an image's
41/// `[image.<id>] env` lands) > Kconfig `CONFIG_NROS_LOG_DYNAMIC_LOGGERS` /
42/// the board's `[knobs.log] dynamic_loggers` > 16. Linux host boards
43/// state 32; MCU boards keep 16. `0` declines the arena entirely (creation
44/// always fails, lookup still works), which is the right build for an image
45/// whose loggers are all `static`.
46///
47/// The `dynamic-loggers-<N>` features are deprecated (one release): honoured
48/// with a warning when no knob is stated, and a build error when they disagree
49/// with one.
50#[must_use]
51pub const fn dynamic_logger_capacity() -> usize {
52    crate::config::DYNAMIC_LOGGER_CAPACITY
53}
54
55/// Name-arena bytes reserved per runtime logger.
56///
57/// An AVERAGE, not a per-name cap ([`MAX_LOGGER_NAME_LEN`] is that): the
58/// arena is one bump region of `capacity * 24` bytes shared by every name.
59const NAME_ARENA_BYTES_PER_LOGGER: usize = 24;
60
61/// Longest logger name the arena will accept, in bytes.
62///
63/// A cap per NAME as well as in total, so one absurd name cannot consume the
64/// budget every other logger in the image needs.
65pub const MAX_LOGGER_NAME_LEN: usize = 48;
66
67const CAPACITY: usize = dynamic_logger_capacity();
68const NAME_ARENA_BYTES: usize = CAPACITY * NAME_ARENA_BYTES_PER_LOGGER;
69
70struct Pool {
71    loggers: UnsafeCell<[MaybeUninit<Logger>; CAPACITY]>,
72    loggers_used: AtomicUsize,
73    names_used: AtomicUsize,
74}
75
76/// The name arena, a static of its OWN rather than a field of [`POOL`].
77///
78/// phase-479 W5 — so its symbol is exactly `capacity * 24` bytes, with no
79/// struct padding and no `Logger` (whose size moves with its fields) beside
80/// it. `just mem-report` reads the image's capacity back out of that size
81/// (`nros_log::pool::NAMES`), which is the only way a STATIC report can state
82/// a count rather than a byte figure.
83struct NameArena(UnsafeCell<[u8; NAME_ARENA_BYTES]>);
84
85// SAFETY: every mutation reserves its region first with a `compare_exchange`
86// on the matching `*_used` counter, so no two writers ever touch the same
87// bytes; a region is written once and never rewritten or freed, so a reader
88// holding a `&'static` into it can never observe a change. Publication to
89// OTHER threads happens through the intern table's `AcqRel` CAS in
90// `crate::get_or_create_logger`, which is the only way another thread can
91// reach one of these references.
92unsafe impl Sync for Pool {}
93
94// SAFETY: as for `Pool` — every region is reserved by a CAS on
95// `POOL.names_used` before it is written, and never written again.
96unsafe impl Sync for NameArena {}
97
98static POOL: Pool = Pool {
99    loggers: UnsafeCell::new([const { MaybeUninit::uninit() }; CAPACITY]),
100    loggers_used: AtomicUsize::new(0),
101    names_used: AtomicUsize::new(0),
102};
103
104static NAMES: NameArena = NameArena(UnsafeCell::new([0_u8; NAME_ARENA_BYTES]));
105
106/// Copy `name` into the static name arena and return a `&'static str` over the
107/// copy. `None` if the name is too long or the arena is exhausted.
108pub(crate) fn intern_name(name: &str) -> Option<&'static str> {
109    let len = name.len();
110    if len == 0 || len > MAX_LOGGER_NAME_LEN || len > NAME_ARENA_BYTES {
111        return None;
112    }
113    loop {
114        let used = POOL.names_used.load(Ordering::Acquire);
115        let end = used.checked_add(len)?;
116        if end > NAME_ARENA_BYTES {
117            return None;
118        }
119        if POOL
120            .names_used
121            .compare_exchange(used, end, Ordering::AcqRel, Ordering::Acquire)
122            .is_err()
123        {
124            continue;
125        }
126        // SAFETY: `[used, end)` is reserved to this call by the CAS above and
127        // is never written again. The arena is a `'static`, so the slice we
128        // build over it lives for the program.
129        let interned: &'static str = unsafe {
130            let base = NAMES.0.get().cast::<u8>();
131            core::ptr::copy_nonoverlapping(name.as_ptr(), base.add(used), len);
132            core::str::from_utf8_unchecked(core::slice::from_raw_parts(base.add(used), len))
133        };
134        return Some(interned);
135    }
136}
137
138/// Move `logger` into the static logger arena and return a `&'static` to it.
139/// `None` when the arena is exhausted.
140// `dynamic-loggers-0` makes the capacity 0, a legal size: the comparison is then
141// always true, which is the right answer for a pool with no slots.
142#[allow(clippy::absurd_extreme_comparisons)]
143pub(crate) fn place(logger: Logger) -> Option<&'static Logger> {
144    loop {
145        let used = POOL.loggers_used.load(Ordering::Acquire);
146        if used >= CAPACITY {
147            return None;
148        }
149        if POOL
150            .loggers_used
151            .compare_exchange(used, used + 1, Ordering::AcqRel, Ordering::Acquire)
152            .is_err()
153        {
154            continue;
155        }
156        // SAFETY: slot `used` is reserved to this call by the CAS above, is
157        // uninitialised until this write, and is never written again.
158        let placed: &'static Logger = unsafe {
159            let base = POOL.loggers.get().cast::<MaybeUninit<Logger>>();
160            let slot = base.add(used);
161            slot.write(MaybeUninit::new(logger));
162            &*slot.cast::<Logger>()
163        };
164        return Some(placed);
165    }
166}
167
168/// How many of the [`dynamic_logger_capacity`] slots are spent.
169///
170/// Counts slots CLAIMED, which is one more than the loggers reachable by name
171/// whenever two threads raced the same new name: the loser's slot is spent and
172/// unreferenced. Bounded and reported rather than reclaimed — this arena has
173/// no free list by construction.
174#[must_use]
175pub fn dynamic_loggers_in_use() -> usize {
176    POOL.loggers_used.load(Ordering::Acquire)
177}
178
179/// Bytes of the name arena spent, and its total size.
180#[must_use]
181pub fn dynamic_logger_name_arena() -> (usize, usize) {
182    (POOL.names_used.load(Ordering::Acquire), NAME_ARENA_BYTES)
183}