Skip to main content

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}