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}