nros_platform_cffi/log.rs
1//! The `LogSink` that speaks the platform ABI.
2//!
3//! ## Why it lives here and not in the facade
4//!
5//! `nros-log` is a facade: `LogSink` + `init` exist so delivery is PLUGGABLE.
6//! `PlatformSink` is one bridge among possible many — it is the only one that
7//! needs `nros_platform_log_write`, which is a LINK-TIME requirement on the
8//! final binary.
9//!
10//! While it lived in `nros-log`, "does this binary need the platform log ABI?"
11//! was answerable only by a Cargo feature, and issue 0710 showed that cannot
12//! work: `nros-platform-cffi` and `nros-rmw-bridge` enable
13//! `nros-node/rmw-cffi` unconditionally, so workspace feature unification
14//! turns any forwarded gate back ON for every member of the build. A feature is
15//! a property of the BUILD.
16//!
17//! A DEPENDENCY is a property of the binary. Here, the question answers itself:
18//! a binary that links this crate has the ABI (that is what this crate is), and
19//! one that does not cannot accidentally acquire the requirement. The extern is
20//! also declared exactly once now, in `generated.rs` — bindgen output from
21//! `<nros/platform.h>`, the SSoT per RFC-0054 — rather than a second time by
22//! hand in the facade.
23//!
24//! ## Using it
25//!
26//! ```ignore
27//! nros_log::init(nros_platform_cffi::log::default_sinks());
28//! // or, equivalently:
29//! nros_platform_cffi::log::init_default();
30//! ```
31//!
32//! Apps wanting fan-out (e.g. platform + `/rosout`) compose their own
33//! `&'static [&dyn LogSink]` and pass it to `nros_log::init`, exactly as before.
34
35use nros_log::{FormatBuffer, LogSink, Record};
36
37/// Forwards every record to the platform port's `nros_platform_log_write`.
38///
39/// Zero-sized. Threading + ISR safety inherit from the linked
40/// `nros-platform-<rtos>` impl — see the table in
41/// `docs/roadmap/archived/phase-88-nros-log.md`.
42pub struct PlatformSink;
43
44impl LogSink for PlatformSink {
45 fn log(&self, record: &Record<'_>) {
46 // Issue #503 — prefix the rendered line with the record's monotonic
47 // stamp as `[sssss.uuuuuu]`. Done by message rewrite because the
48 // `nros_platform_log_write` ABI has no timestamp parameter and widening
49 // it would touch every platform port; the prefix is additive, not a
50 // re-format of the caller's text.
51 //
52 // Keyed on the STAMP, not on a Cargo feature. In `nros-log` this was
53 // `#[cfg(feature = "platform-clock")]`, which cannot follow the sink
54 // across a crate boundary without inventing a second feature that means
55 // the same thing. A record with no stamp (`0`) is already the "no clock"
56 // case, so the runtime check subsumes the cfg — and one branch on a u64
57 // is not what a log path is spending its time on.
58 if record.timestamp_ns != 0 {
59 use core::fmt::Write as _;
60 let secs = record.timestamp_ns / 1_000_000_000;
61 let micros = (record.timestamp_ns % 1_000_000_000) / 1_000;
62 let mut buf = FormatBuffer::new();
63 let _ = core::write!(buf, "[{secs:5}.{micros:06}] {}", record.message);
64 emit(record.severity.as_u8(), record.logger_name, buf.as_str());
65 return;
66 }
67 emit(record.severity.as_u8(), record.logger_name, record.message);
68 }
69
70 fn flush(&self) {
71 // SAFETY: no args, no preconditions.
72 unsafe { crate::generated::nros_platform_log_flush() };
73 }
74}
75
76fn emit(severity: u8, name: &str, msg: &str) {
77 let name = name.as_bytes();
78 let msg = msg.as_bytes();
79 // SAFETY: pointers come from `&str` references that outlive the call;
80 // lengths match.
81 unsafe {
82 crate::generated::nros_platform_log_write(
83 severity,
84 name.as_ptr(),
85 name.len(),
86 msg.as_ptr(),
87 msg.len(),
88 );
89 }
90}
91
92static PLATFORM_SINK: PlatformSink = PlatformSink;
93
94/// The default sink list: just [`PlatformSink`].
95#[must_use]
96pub fn default_sinks() -> &'static [&'static dyn LogSink] {
97 static SINKS: &[&dyn LogSink] = &[&PLATFORM_SINK];
98 SINKS
99}
100
101/// Install [`default_sinks`] as the global sink list.
102///
103/// Convenience for the common boot funnel; equivalent to
104/// `nros_log::init(default_sinks())`, and it drains anything
105/// `nros_log::early` held before this point.
106pub fn init_default() {
107 nros_log::init(default_sinks());
108}