Skip to main content

nros_log/
macros.rs

1//! Phase 88.2 — the emission macros.
2//!
3//! Each macro formats its arguments into a stack-resident
4//! [`crate::FormatBuffer`] (capacity picked at compile time by the
5//! `buffer-size-<N>` feature), wraps the result in a [`crate::Record`],
6//! and hands it to the [`crate::Logger`]'s dispatcher.
7//!
8//! Compile-time ceiling: macros below
9//! [`crate::compile_time_ceiling`](super::severity_enabled_at_compile_time)
10//! expand to `()` — the format call is dead-code-eliminated.
11//!
12//! ## Naming (RFC-0089, settled 2026-09-04)
13//!
14//! The three ROS 2 client libraries disagree with each other about how to
15//! spell a log line:
16//!
17//! ```text
18//! rclrs    log_info!(logger, "…")        <- Rust follows THIS
19//! rclcpp   RCLCPP_INFO(logger, "…")
20//! rcutils  RCUTILS_LOG_INFO_NAMED(…)
21//! ```
22//!
23//! There is no single "ROS 2 spelling", so each of our languages follows ITS
24//! OWN upstream. For Rust that is rclrs, and rclrs's five severity macros are
25//! [`log_debug!`](crate::log_debug), [`log_info!`](crate::log_info),
26//! [`log_warn!`](crate::log_warn), [`log_error!`](crate::log_error) and
27//! [`log_fatal!`](crate::log_fatal) — which is what this module now exports.
28//! (They carry an explicit target because `#[macro_export]` puts a macro at
29//! the CRATE ROOT, not in the module whose file declares it.) The pre-rename
30//! `nros_debug!` .. `nros_fatal!` forwarders, and the
31//! `deprecate-legacy-names` feature that armed their `#[deprecated]`, were
32//! removed in phase-417 W-B5 once every in-tree call site had moved. There is
33//! no `log_trace!`: see below.
34//!
35//! **A name takes rclrs's spelling exactly when rclrs HAS that name.** Two
36//! families deliberately keep the `nros_` prefix, because inventing a name
37//! inside rclrs's namespace-alike would claim an upstream twin that does not
38//! exist:
39//!
40//! * [`nros_trace!`](crate::nros_trace) — rclrs stops at `debug`. TRACE is ours (ledger
41//!   `rust:nros_trace`, verdict `extension`), so it keeps a name that says so.
42//! * the `nros_*_throttle!` / `nros_*_throttle_at!` family — rclrs expresses
43//!   throttling as a modifier on ONE macro
44//!   (`log_info!(logger.throttle(Duration::from_millis(500)), "…")` via
45//!   `LogParams` / `ToLogParams`), not as a macro per severity. Ours is a
46//!   different DECOMPOSITION of the same capability, already ledgered as
47//!   `divergence`; see that family's own note for what adopting rclrs's shape
48//!   would cost.
49
50/// Internal macro emitting one log record at `$severity`.
51///
52/// Use the named helpers ([`nros_trace!`](crate::nros_trace),
53/// [`log_debug!`](crate::log_debug), etc.) — they
54/// gate on the compile-time ceiling before evaluating this body.
55#[doc(hidden)]
56#[macro_export]
57macro_rules! __nros_log_emit {
58    ($logger:expr, $severity:expr, $($arg:tt)+) => {{
59        // Cheap runtime threshold check first to short-circuit
60        // disabled call sites before formatting.
61        let __logger: &$crate::Logger = $logger;
62        let __sev = $severity;
63        if __logger.is_enabled(__sev) {
64            use ::core::fmt::Write as _;
65            let mut __buf = $crate::FormatBuffer::new();
66            // Ignoring `write!` result — `FormatBuffer::write_str`
67            // never returns `Err`; overflow is signalled via the
68            // `truncated()` accessor (not the `Result`).
69            let _ = ::core::write!(__buf, $($arg)+);
70            let __record = $crate::Record {
71                severity:     __sev,
72                logger_name:  __logger.name(),
73                message:      __buf.as_str(),
74                file:         ::core::file!(),
75                line:         ::core::line!(),
76                timestamp_ns: $crate::__timestamp_ns(),
77            };
78            __logger.dispatch(&__record);
79        }
80    }};
81}
82
83/// Emit one record at a severity chosen at RUNTIME — rclrs's `log!`.
84///
85/// phase-417 G6, ledger row `rust:log`. The severity-taking emitter existed as
86/// [`__nros_log_emit!`], `#[doc(hidden)]` and named as an implementation
87/// detail, so a ported `log!(logger, severity, …)` had no supported spelling
88/// to land on. This is that spelling.
89///
90/// ```ignore
91/// let severity = if recovered { Severity::Info } else { Severity::Error };
92/// nros_log::log!(logger, severity, "link {}", state);
93/// ```
94///
95/// **It routes through exactly what the five level macros route through**, and
96/// that is the whole risk in a macro this cheap: the compile-time ceiling, the
97/// runtime `is_enabled` threshold and the one `FormatBuffer`. If it grew its
98/// own gate, `log!(logger, Severity::Debug, …)` would obey a different
99/// threshold from [`log_debug!`](crate::log_debug) and the two spellings of one call would
100/// disagree — which is what a second implementation of a gate always ends up
101/// meaning.
102///
103/// MEASURED, and worth knowing before trusting the `is_enabled` half of that
104/// sentence: [`crate::Logger::dispatch`] re-checks the threshold itself, so
105/// removing the check inside [`__nros_log_emit!`] changes nothing observable.
106/// The runtime gate is belt-and-braces; what a second implementation could
107/// actually get wrong here is the COMPILE-TIME ceiling and the severity the
108/// record carries, and those are what the test asserts.
109///
110/// **One difference from the five, forced by the runtime severity.** Those
111/// consult [`crate::severity_enabled_at_compile_time`] with a CONSTANT, so a
112/// call below the ceiling compiles to `()` and the formatting is
113/// dead-code-eliminated. Here the severity is a value, so the same check
114/// happens — at run time, on a `Severity` the compiler does not know. A call
115/// site that wants the record and its arguments gone from the image entirely
116/// names the level macro; this one keeps the `write!` in the binary and
117/// decides per call.
118///
119/// A THROTTLED severity-taking form is deliberately absent: our throttling is
120/// a macro per severity (`nros_*_throttle!`), a DECOMPOSITION already ledgered
121/// as `divergence` against rclrs's `logger.throttle(…)` modifier, and adding a
122/// severity-taking member to that family would commit to the modifier shape in
123/// one place only.
124#[macro_export]
125macro_rules! log {
126    ($logger:expr, $severity:expr, $($arg:tt)+) => {{
127        let __sev = $severity;
128        if $crate::severity_enabled_at_compile_time(__sev) {
129            $crate::__nros_log_emit!($logger, __sev, $($arg)+);
130        }
131    }};
132}
133
134/// Emit at [`crate::Severity::Trace`].
135///
136/// Disabled at compile time unless the `max-level-trace` feature is
137/// the active ceiling (default).
138#[macro_export]
139macro_rules! nros_trace {
140    ($logger:expr, $($arg:tt)+) => {
141        if $crate::severity_enabled_at_compile_time($crate::Severity::Trace) {
142            $crate::__nros_log_emit!($logger, $crate::Severity::Trace, $($arg)+);
143        }
144    };
145}
146
147/// Emit at [`crate::Severity::Debug`].
148///
149/// `rclrs::log_debug!`'s spelling — see the module note on RFC-0089's
150/// "each language follows its own upstream" rule. The pre-rename
151/// `nros_debug!` forwarder was removed in phase-417 W-B5.
152#[macro_export]
153macro_rules! log_debug {
154    ($logger:expr, $($arg:tt)+) => {
155        if $crate::severity_enabled_at_compile_time($crate::Severity::Debug) {
156            $crate::__nros_log_emit!($logger, $crate::Severity::Debug, $($arg)+);
157        }
158    };
159}
160
161/// Emit at [`crate::Severity::Info`].
162///
163/// `rclrs::log_info!`'s spelling — see the module note on RFC-0089's
164/// "each language follows its own upstream" rule. The pre-rename
165/// `nros_info!` forwarder was removed in phase-417 W-B5.
166#[macro_export]
167macro_rules! log_info {
168    ($logger:expr, $($arg:tt)+) => {
169        if $crate::severity_enabled_at_compile_time($crate::Severity::Info) {
170            $crate::__nros_log_emit!($logger, $crate::Severity::Info, $($arg)+);
171        }
172    };
173}
174
175/// Emit at [`crate::Severity::Warn`].
176///
177/// `rclrs::log_warn!`'s spelling — see the module note on RFC-0089's
178/// "each language follows its own upstream" rule. The pre-rename
179/// `nros_warn!` forwarder was removed in phase-417 W-B5.
180#[macro_export]
181macro_rules! log_warn {
182    ($logger:expr, $($arg:tt)+) => {
183        if $crate::severity_enabled_at_compile_time($crate::Severity::Warn) {
184            $crate::__nros_log_emit!($logger, $crate::Severity::Warn, $($arg)+);
185        }
186    };
187}
188
189/// Emit at [`crate::Severity::Error`].
190///
191/// `rclrs::log_error!`'s spelling — see the module note on RFC-0089's
192/// "each language follows its own upstream" rule. The pre-rename
193/// `nros_error!` forwarder was removed in phase-417 W-B5.
194#[macro_export]
195macro_rules! log_error {
196    ($logger:expr, $($arg:tt)+) => {
197        if $crate::severity_enabled_at_compile_time($crate::Severity::Error) {
198            $crate::__nros_log_emit!($logger, $crate::Severity::Error, $($arg)+);
199        }
200    };
201}
202
203/// Emit at [`crate::Severity::Fatal`].
204///
205/// `rclrs::log_fatal!`'s spelling — see the module note on RFC-0089's
206/// "each language follows its own upstream" rule. The pre-rename
207/// `nros_fatal!` forwarder was removed in phase-417 W-B5.
208#[macro_export]
209macro_rules! log_fatal {
210    ($logger:expr, $($arg:tt)+) => {
211        if $crate::severity_enabled_at_compile_time($crate::Severity::Fatal) {
212            $crate::__nros_log_emit!($logger, $crate::Severity::Fatal, $($arg)+);
213        }
214    };
215}
216
217// -----------------------------------------------------------------------------
218// phase-417 W4.d — throttled emission.
219//
220// There WAS a throttle in this tree before: `nros_core::logger::Logger::
221// *_throttle`. It sat on a DIFFERENT `Logger` — the one that forwards to the
222// `log` crate — so it never reached `nros_platform_log_write`, and the
223// `Logger` the FFI hands to C and C++ had no throttle at all. Two types, the
224// capability on the unused one. The rule itself now lives in
225// [`crate::throttle`], which both this macro family and the C shim call.
226//
227// Two spellings, and the difference is WHERE THE TIME COMES FROM:
228//
229// * `nros_*_throttle!(logger, interval_ms, ...)` reads the platform clock.
230// * `nros_*_throttle_at!(logger, now_ns, interval_ms, ...)` takes it, which is
231//   `rclcpp`'s shape (`RCLCPP_INFO_THROTTLE(logger, clock, ...)` names the
232//   clock) and the only form available with no platform port.
233//
234// The clock-reading form REFUSES TO COMPILE without the platform clock
235// (`cfg(nros_log_clock)`, issue 1037), rather than compiling into a throttle with no time base. Without a
236// clock `__timestamp_ns()` is a constant `0`, and a window measured against a
237// constant never elapses — the site emits its first record and then nothing,
238// forever (issue 1152; before it, the same constant admitted EVERY record),
239// while reading at the call site exactly like a working throttle. RFC-0089:
240// never compile and differ.
241//
242// WHY THIS FAMILY KEEPS THE `nros_` PREFIX WHILE THE FIVE SEVERITY MACROS TOOK
243// rclrs's (phase-417, 2026-09-04). rclrs has no `log_info_throttle!` to follow.
244// Its throttle is a MODIFIER on the one macro — `log_info!(logger.throttle(
245// Duration::from_millis(500)), "…")`, where the first argument is anything
246// implementing `ToLogParams` — so adopting "rclrs's spelling" here means
247// adopting `ToLogParams` and `LogParams`, not renaming a macro. That is a
248// SHAPE change, and three ledger rows have already decided against the pieces
249// it needs, each on a platform ground:
250//
251//   * `rust:ToLogParams` — `declined`, an ergonomic overload trait over what
252//     is one concrete caller shape here (`&Logger`).
253//   * `rust:LogOccurrence` — `divergence`; `once` / `skip_first` take a
254//     CALLER-SUPPLIED `&OnceFlag` because riscv32imc has no CAS, so the
255//     occurrence state cannot live inside a params value.
256//   * `rust:ThrottleClock` — `divergence`; rclrs's `LogParams` names the clock
257//     a window is measured against and we have none to name.
258//
259// A `LogParams` missing those members is not rclrs's `LogParams`; it is a new
260// divergence wearing an upstream name, which is the "compiles and differs"
261// RFC-0089 exists to forbid. There is also a live cost: in the modifier form
262// whether a throttle was requested is a RUNTIME field, so the `platform-clock`
263// refusal above stops being expressible at the call site — the exact
264// silently-admits-everything failure the `compile_error!` was added to prevent.
265//
266// So: ours is KEPT, and it keeps the `nros_` prefix that says it is ours. The
267// ledger already records it as a `divergence` with a `provides` arrow at
268// `rust:nros_debug_throttle`; that verdict stands unchanged.
269// -----------------------------------------------------------------------------
270
271/// Resolves the timestamp the `nros_*_throttle!` family measures against.
272///
273/// With the platform clock (`cfg(nros_log_clock)` — the platform's `clock`
274/// capability or the `platform-clock` feature, issue 1037): the platform's
275/// monotonic clock. Without: a compile error — see the family's module note
276/// for why this is not a silent degradation.
277#[cfg(nros_log_clock)]
278#[doc(hidden)]
279#[macro_export]
280macro_rules! __nros_throttle_now {
281    () => {
282        $crate::__timestamp_ns()
283    };
284}
285
286/// See the `nros_log_clock` arm above.
287#[cfg(not(nros_log_clock))]
288#[doc(hidden)]
289#[macro_export]
290macro_rules! __nros_throttle_now {
291    () => {
292        ::core::compile_error!(
293            "nros_*_throttle! needs a monotonic clock: build for a platform whose \
294             nros-platform.toml declares `[capabilities] clock = true` (the lane names it through \
295             NROS_PLATFORM_NAME), enable nros-log's `platform-clock` feature where a port is linked, \
296             or use nros_*_throttle_at!(logger, now_ns, interval_ms, ...) and pass the \
297             time yourself. Without a clock `__timestamp_ns()` is a constant 0, so the window \
298             would admit every record while looking exactly like a working throttle."
299        )
300    };
301}
302
303/// Internal macro emitting one throttled record at `$severity`.
304///
305/// `$state` is a `&'static ThrottleState`; the public macros declare it as a
306/// `static` at the call site, which is what makes the window per-SITE rather
307/// than per-logger.
308#[doc(hidden)]
309#[macro_export]
310macro_rules! __nros_log_emit_throttled {
311    ($logger:expr, $severity:expr, $state:expr, $now_ns:expr, $interval_ms:expr, $($arg:tt)+) => {{
312        let __logger: &$crate::Logger = $logger;
313        let __sev = $severity;
314        let __now: u64 = $now_ns;
315        let __interval = $crate::interval_ms_to_ns($interval_ms as u64);
316        // Severity first, window second — see `Logger::is_enabled_throttled`.
317        // Also short-circuits the format call, like the unthrottled macros.
318        if __logger.is_enabled_throttled(__sev, $state, __now, __interval) {
319            use ::core::fmt::Write as _;
320            let mut __buf = $crate::FormatBuffer::new();
321            let _ = ::core::write!(__buf, $($arg)+);
322            let __record = $crate::Record {
323                severity:     __sev,
324                logger_name:  __logger.name(),
325                message:      __buf.as_str(),
326                file:         ::core::file!(),
327                line:         ::core::line!(),
328                // The timestamp the window was decided against, not a second
329                // reading — two clock reads for one record is how a record can
330                // be stamped before the window it opened.
331                timestamp_ns: __now,
332            };
333            __logger.dispatch(&__record);
334        }
335    }};
336}
337
338/// Declares the per-call-site window and forwards. Internal.
339#[doc(hidden)]
340#[macro_export]
341macro_rules! __nros_throttle_site {
342    ($logger:expr, $severity:expr, $now_ns:expr, $interval_ms:expr, $($arg:tt)+) => {{
343        static __NROS_THROTTLE_SITE: $crate::ThrottleState = $crate::ThrottleState::new();
344        $crate::__nros_log_emit_throttled!(
345            $logger, $severity, &__NROS_THROTTLE_SITE, $now_ns, $interval_ms, $($arg)+
346        );
347    }};
348}
349
350/// Emit at [`crate::Severity::Trace`], at most once per `interval_ms` at this
351/// call site.
352///
353/// Reads the platform clock; needs `cfg(nros_log_clock)` (see
354/// [`nros_trace_throttle_at!`](crate::nros_trace_throttle_at) for the form that takes the time).
355#[macro_export]
356macro_rules! nros_trace_throttle {
357    ($logger:expr, $interval_ms:expr, $($arg:tt)+) => {
358        if $crate::severity_enabled_at_compile_time($crate::Severity::Trace) {
359            $crate::__nros_throttle_site!(
360                $logger,
361                $crate::Severity::Trace,
362                $crate::__nros_throttle_now!(),
363                $interval_ms,
364                $($arg)+
365            );
366        }
367    };
368}
369
370/// Emit at [`crate::Severity::Trace`], at most once per `interval_ms` at this
371/// call site, measured against the `now_ns` the CALLER supplies.
372///
373/// `rclcpp`'s shape — `RCLCPP_TRACE_THROTTLE(logger, clock, …)` names its clock
374/// too — and the only throttled form available with no platform clock.
375#[macro_export]
376macro_rules! nros_trace_throttle_at {
377    ($logger:expr, $now_ns:expr, $interval_ms:expr, $($arg:tt)+) => {
378        if $crate::severity_enabled_at_compile_time($crate::Severity::Trace) {
379            $crate::__nros_throttle_site!(
380                $logger, $crate::Severity::Trace, $now_ns, $interval_ms, $($arg)+
381            );
382        }
383    };
384}
385
386/// Emit at [`crate::Severity::Debug`], at most once per `interval_ms` at this
387/// call site.
388///
389/// Reads the platform clock; needs `cfg(nros_log_clock)` (see
390/// [`nros_debug_throttle_at!`](crate::nros_debug_throttle_at) for the form that takes the time).
391#[macro_export]
392macro_rules! nros_debug_throttle {
393    ($logger:expr, $interval_ms:expr, $($arg:tt)+) => {
394        if $crate::severity_enabled_at_compile_time($crate::Severity::Debug) {
395            $crate::__nros_throttle_site!(
396                $logger,
397                $crate::Severity::Debug,
398                $crate::__nros_throttle_now!(),
399                $interval_ms,
400                $($arg)+
401            );
402        }
403    };
404}
405
406/// Emit at [`crate::Severity::Debug`], at most once per `interval_ms` at this
407/// call site, measured against the `now_ns` the CALLER supplies.
408///
409/// `rclcpp`'s shape — `RCLCPP_DEBUG_THROTTLE(logger, clock, …)` names its clock
410/// too — and the only throttled form available with no platform clock.
411#[macro_export]
412macro_rules! nros_debug_throttle_at {
413    ($logger:expr, $now_ns:expr, $interval_ms:expr, $($arg:tt)+) => {
414        if $crate::severity_enabled_at_compile_time($crate::Severity::Debug) {
415            $crate::__nros_throttle_site!(
416                $logger, $crate::Severity::Debug, $now_ns, $interval_ms, $($arg)+
417            );
418        }
419    };
420}
421
422/// Emit at [`crate::Severity::Info`], at most once per `interval_ms` at this
423/// call site.
424///
425/// Reads the platform clock; needs `cfg(nros_log_clock)` (see
426/// [`nros_info_throttle_at!`](crate::nros_info_throttle_at) for the form that takes the time).
427#[macro_export]
428macro_rules! nros_info_throttle {
429    ($logger:expr, $interval_ms:expr, $($arg:tt)+) => {
430        if $crate::severity_enabled_at_compile_time($crate::Severity::Info) {
431            $crate::__nros_throttle_site!(
432                $logger,
433                $crate::Severity::Info,
434                $crate::__nros_throttle_now!(),
435                $interval_ms,
436                $($arg)+
437            );
438        }
439    };
440}
441
442/// Emit at [`crate::Severity::Info`], at most once per `interval_ms` at this
443/// call site, measured against the `now_ns` the CALLER supplies.
444///
445/// `rclcpp`'s shape — `RCLCPP_INFO_THROTTLE(logger, clock, …)` names its clock
446/// too — and the only throttled form available with no platform clock.
447#[macro_export]
448macro_rules! nros_info_throttle_at {
449    ($logger:expr, $now_ns:expr, $interval_ms:expr, $($arg:tt)+) => {
450        if $crate::severity_enabled_at_compile_time($crate::Severity::Info) {
451            $crate::__nros_throttle_site!(
452                $logger, $crate::Severity::Info, $now_ns, $interval_ms, $($arg)+
453            );
454        }
455    };
456}
457
458/// Emit at [`crate::Severity::Warn`], at most once per `interval_ms` at this
459/// call site.
460///
461/// Reads the platform clock; needs `cfg(nros_log_clock)` (see
462/// [`nros_warn_throttle_at!`](crate::nros_warn_throttle_at) for the form that takes the time).
463#[macro_export]
464macro_rules! nros_warn_throttle {
465    ($logger:expr, $interval_ms:expr, $($arg:tt)+) => {
466        if $crate::severity_enabled_at_compile_time($crate::Severity::Warn) {
467            $crate::__nros_throttle_site!(
468                $logger,
469                $crate::Severity::Warn,
470                $crate::__nros_throttle_now!(),
471                $interval_ms,
472                $($arg)+
473            );
474        }
475    };
476}
477
478/// Emit at [`crate::Severity::Warn`], at most once per `interval_ms` at this
479/// call site, measured against the `now_ns` the CALLER supplies.
480///
481/// `rclcpp`'s shape — `RCLCPP_WARN_THROTTLE(logger, clock, …)` names its clock
482/// too — and the only throttled form available with no platform clock.
483#[macro_export]
484macro_rules! nros_warn_throttle_at {
485    ($logger:expr, $now_ns:expr, $interval_ms:expr, $($arg:tt)+) => {
486        if $crate::severity_enabled_at_compile_time($crate::Severity::Warn) {
487            $crate::__nros_throttle_site!(
488                $logger, $crate::Severity::Warn, $now_ns, $interval_ms, $($arg)+
489            );
490        }
491    };
492}
493
494/// Emit at [`crate::Severity::Error`], at most once per `interval_ms` at this
495/// call site.
496///
497/// Reads the platform clock; needs `cfg(nros_log_clock)` (see
498/// [`nros_error_throttle_at!`](crate::nros_error_throttle_at) for the form that takes the time).
499#[macro_export]
500macro_rules! nros_error_throttle {
501    ($logger:expr, $interval_ms:expr, $($arg:tt)+) => {
502        if $crate::severity_enabled_at_compile_time($crate::Severity::Error) {
503            $crate::__nros_throttle_site!(
504                $logger,
505                $crate::Severity::Error,
506                $crate::__nros_throttle_now!(),
507                $interval_ms,
508                $($arg)+
509            );
510        }
511    };
512}
513
514/// Emit at [`crate::Severity::Error`], at most once per `interval_ms` at this
515/// call site, measured against the `now_ns` the CALLER supplies.
516///
517/// `rclcpp`'s shape — `RCLCPP_ERROR_THROTTLE(logger, clock, …)` names its clock
518/// too — and the only throttled form available with no platform clock.
519#[macro_export]
520macro_rules! nros_error_throttle_at {
521    ($logger:expr, $now_ns:expr, $interval_ms:expr, $($arg:tt)+) => {
522        if $crate::severity_enabled_at_compile_time($crate::Severity::Error) {
523            $crate::__nros_throttle_site!(
524                $logger, $crate::Severity::Error, $now_ns, $interval_ms, $($arg)+
525            );
526        }
527    };
528}
529
530/// Emit at [`crate::Severity::Fatal`], at most once per `interval_ms` at this
531/// call site.
532///
533/// Reads the platform clock; needs `cfg(nros_log_clock)` (see
534/// [`nros_fatal_throttle_at!`](crate::nros_fatal_throttle_at) for the form that takes the time).
535#[macro_export]
536macro_rules! nros_fatal_throttle {
537    ($logger:expr, $interval_ms:expr, $($arg:tt)+) => {
538        if $crate::severity_enabled_at_compile_time($crate::Severity::Fatal) {
539            $crate::__nros_throttle_site!(
540                $logger,
541                $crate::Severity::Fatal,
542                $crate::__nros_throttle_now!(),
543                $interval_ms,
544                $($arg)+
545            );
546        }
547    };
548}
549
550/// Emit at [`crate::Severity::Fatal`], at most once per `interval_ms` at this
551/// call site, measured against the `now_ns` the CALLER supplies.
552///
553/// `rclcpp`'s shape — `RCLCPP_FATAL_THROTTLE(logger, clock, …)` names its clock
554/// too — and the only throttled form available with no platform clock.
555#[macro_export]
556macro_rules! nros_fatal_throttle_at {
557    ($logger:expr, $now_ns:expr, $interval_ms:expr, $($arg:tt)+) => {
558        if $crate::severity_enabled_at_compile_time($crate::Severity::Fatal) {
559            $crate::__nros_throttle_site!(
560                $logger, $crate::Severity::Fatal, $now_ns, $interval_ms, $($arg)+
561            );
562        }
563    };
564}