|
nros C API
Lightweight ROS 2 client for embedded real-time systems
|
Phase 88.12 — ROS 2 style leveled logging surface for the C API. More...
#include <stdarg.h>#include <stdbool.h>#include <stddef.h>#include <stdint.h>

Go to the source code of this file.
Macros | |
| #define | NROS_LOG_FMT_BUFFER_SIZE 256 |
| #define | RCLC_LOG_DEBUG(logger, ...) |
| #define | RCLC_LOG_DEBUG_THROTTLE(logger, interval_ms, ...) RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_DEBUG, (interval_ms), __VA_ARGS__) |
| #define | RCLC_LOG_ERROR(logger, ...) |
| #define | RCLC_LOG_ERROR_THROTTLE(logger, interval_ms, ...) RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_ERROR, (interval_ms), __VA_ARGS__) |
| #define | RCLC_LOG_FATAL(logger, ...) |
| #define | RCLC_LOG_FATAL_THROTTLE(logger, interval_ms, ...) RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_FATAL, (interval_ms), __VA_ARGS__) |
| #define | RCLC_LOG_INFO(logger, ...) |
| #define | RCLC_LOG_INFO_THROTTLE(logger, interval_ms, ...) RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_INFO, (interval_ms), __VA_ARGS__) |
| #define | RCLC_LOG_THROTTLE_AT(logger, severity, interval_ms, ...) |
| #define | RCLC_LOG_TRACE(logger, ...) |
| #define | RCLC_LOG_TRACE_THROTTLE(logger, interval_ms, ...) RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_TRACE, (interval_ms), __VA_ARGS__) |
| #define | RCLC_LOG_WARN(logger, ...) |
| #define | RCLC_LOG_WARN_THROTTLE(logger, interval_ms, ...) RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_WARN, (interval_ms), __VA_ARGS__) |
Typedefs | |
| typedef void(* | rclc_log_sink_fn) (void *user_data, rcl_log_severity_t severity, const char *logger_name, size_t logger_name_len, const char *message, size_t message_len, const char *file, size_t file_len, uint32_t line, uint64_t timestamp_ns) |
| typedef const void * | rclc_logger_t |
Enumerations | |
| enum | rcl_log_severity_t { RCLC_LOG_SEVERITY_UNSET = 0 , RCLC_LOG_SEVERITY_TRACE = 5 , RCLC_LOG_SEVERITY_DEBUG = 10 , RCLC_LOG_SEVERITY_INFO = 20 , RCLC_LOG_SEVERITY_WARN = 30 , RCLC_LOG_SEVERITY_ERROR = 40 , RCLC_LOG_SEVERITY_FATAL = 50 , RCLC_LOG_SEVERITY_FORCE_INT_WIDTH_ = 0x7fffffff } |
Phase 88.12 — ROS 2 style leveled logging surface for the C API.
Mirrors the Rust [nros_log] facade. Each rcl_node_t exposes a &'static nros_log::Logger via rclc_node_get_logger(node); the NROS_LOG_* macros render the message, attach severity + line info, and dispatch through the same per-platform sink chain as Rust call sites (see Phase 88.5 onwards).
Usage:
Per-platform delivery (POSIX stderr / Zephyr LOG / ESP-IDF esp_log_write / NuttX syslog / FreeRTOS+ThreadX+bare-metal board fn-ptr) is invisible at the C API layer — the dispatcher routes through nros_platform_log_write (declared in <nros/platform.h>).
| #define NROS_LOG_FMT_BUFFER_SIZE 256 |
Stack buffer the located printf-style entry point below renders into.
NOT the NROS_LOG_BUFFER_SIZE knob (issue 1037): c-stubs/log_fmt.c renders into this from a header with no per-build value to read, so its frame stays 256. The knob sizes nros-log's Rust formatter and the slots that store records, so a C record is bounded by the smaller of the two.
| #define RCLC_LOG_DEBUG | ( | logger, | |
| ... | |||
| ) |
| #define RCLC_LOG_DEBUG_THROTTLE | ( | logger, | |
| interval_ms, | |||
| ... | |||
| ) | RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_DEBUG, (interval_ms), __VA_ARGS__) |
| #define RCLC_LOG_ERROR | ( | logger, | |
| ... | |||
| ) |
| #define RCLC_LOG_ERROR_THROTTLE | ( | logger, | |
| interval_ms, | |||
| ... | |||
| ) | RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_ERROR, (interval_ms), __VA_ARGS__) |
| #define RCLC_LOG_FATAL | ( | logger, | |
| ... | |||
| ) |
| #define RCLC_LOG_FATAL_THROTTLE | ( | logger, | |
| interval_ms, | |||
| ... | |||
| ) | RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_FATAL, (interval_ms), __VA_ARGS__) |
| #define RCLC_LOG_INFO | ( | logger, | |
| ... | |||
| ) |
| #define RCLC_LOG_INFO_THROTTLE | ( | logger, | |
| interval_ms, | |||
| ... | |||
| ) | RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_INFO, (interval_ms), __VA_ARGS__) |
| #define RCLC_LOG_THROTTLE_AT | ( | logger, | |
| severity, | |||
| interval_ms, | |||
| ... | |||
| ) |
| #define RCLC_LOG_TRACE | ( | logger, | |
| ... | |||
| ) |
| #define RCLC_LOG_TRACE_THROTTLE | ( | logger, | |
| interval_ms, | |||
| ... | |||
| ) | RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_TRACE, (interval_ms), __VA_ARGS__) |
| #define RCLC_LOG_WARN | ( | logger, | |
| ... | |||
| ) |
| #define RCLC_LOG_WARN_THROTTLE | ( | logger, | |
| interval_ms, | |||
| ... | |||
| ) | RCLC_LOG_THROTTLE_AT((logger), RCLC_LOG_SEVERITY_WARN, (interval_ms), __VA_ARGS__) |
| typedef void(* rclc_log_sink_fn) (void *user_data, rcl_log_severity_t severity, const char *logger_name, size_t logger_name_len, const char *message, size_t message_len, const char *file, size_t file_len, uint32_t line, uint64_t timestamp_ns) |
A C log sink — see rclc_log_add_sink.
Flat arguments rather than a const nros_log_record_t * on purpose: a struct crossing this FFI would be hand-mirrored (cbindgen skips the logging module), and a mirror that misses an appended field passes a SHORTER struct than the reader reads — issue 0160's class, which has bitten three times. With flat arguments an appended field changes this TYPE, so every sink fails to compile instead of reading stack garbage.
logger_name, message and file are pointer + length and are NOT NUL-terminated. They are valid only for the duration of the call.
timestamp_ns is 0 when no monotonic clock is linked — see rclc_log_timestamp_available().
| typedef const void* rclc_logger_t |
Opaque handle to a &'static nros_log::Logger. Obtain via rclc_node_get_logger(...). NEVER free.
| enum rcl_log_severity_t |
Severity levels, on rcutils's number line. Lower value = more verbose.
phase-417 stage 3 (ledger row c:log_severity_t). rcutils — which rcl_log_severity_t mirrors — numbers UNSET=0, DEBUG=10, INFO=20, WARN=30, ERROR=40, FATAL=50, and these values cross ABIs as plain integers: read from a configuration file, read from the environment, or written by a ported file as an RCUTILS_LOG_SEVERITY_* constant. These were a dense 0..=5 until stage 3, so 0 collided with rcutils's UNSET and a ported UNSET silently selected the most verbose level.
The gaps are load-bearing — they are how a caller writes a threshold BETWEEN two named levels. A value in a gap resolves to the named level at or below it, and a value outside the range resolves rather than being rejected, so any int a C caller can produce here is defined.
RCLC_LOG_SEVERITY_TRACE is ours; rcutils has no trace level. It takes 5, inside rcutils's own UNSET..DEBUG gap, which is what that gap is for.
**UNSET MEANS INHERIT, NOT "THE FLOOR"** (phase-467; it meant the floor until then, and that was ledger row c:log_severity_t's whole debt). rclc_logger_set_level(logger, RCLC_LOG_SEVERITY_UNSET) takes away that logger's own level, after which it filters on its nearest ancestor's level (below), else the PROCESS DEFAULT — rclc_log_get_default_level(), RCLC_LOG_SEVERITY_INFO until rclc_log_set_default_level() moves it, which is the value and the starting point RCUTILS_DEFAULT_LOGGER_DEFAULT_LEVEL has upstream. A logger you never set a level on is already in that state.
ENVELOPE — dotted ancestry resolves through loggers that exist. As upstream, an unset logger takes the level of its nearest dotted ancestor (x.y.z → x.y → x) and only then the process default (RFC-0102 D2). Setting nav's level moves nav.costmap's too, unless nav.costmap has its own. The bound: upstream keys a level by NAME in an allocator-backed map, so an ancestor can carry a level without being a logger; here a level lives in a logger, so an ancestor carries one by EXISTING — and setting a level by name (rclc_log_get_logger("nav") then rclc_logger_set_level) creates it, which is the case that matters. The links are made when a logger is created, in either order, so the log path never parses a name. Names are capped at 48 bytes and the runtime-logger arena is bounded; see rclc_logger_get_child. (RFC-0089: the envelope is part of the API, not a footnote.)
UNSET is only special where a LEVEL IS STORED. Passed as a record's severity (rclc_log_emit) or as a threshold question (rclc_logger_is_enabled) it resolves by band like any other integer, which puts it at TRACE: "no level set" is not a level to emit at, upstream has no answer for that call either, and refusing it would make one of these entry points reject a value the other needs.
| bool rclc_log_add_sink | ( | rclc_log_sink_fn | sink, |
| void * | user_data | ||
| ) |
Install a C callback as a log sink.
Until phase-417 the C surface could not install a sink at all: rclc_log_init takes no arguments and hardcodes the platform default. This APPENDS, so a consumer teeing records to its own transport does not discard whatever the board installed.
Records raised before any sink existed are replayed into the first one installed, so a sink registered at boot still sees the boot story.
| sink | The callback. NULL installs nothing and returns false. |
| user_data | Passed back untouched, never dereferenced here. Must outlive the program: a sink cannot be removed (the read path is lock-free). |
sink is NULL or the registry is full. CHECK IT — a sink that was silently not installed is indistinguishable from one that never fires. | rclc_logger_t rclc_log_default_logger | ( | void | ) |
Phase 88.16.H — opaque handle to the catch-all nros logger.
Lets C callers emit through NROS_LOG_* without standing up a full rcl_node_t (useful for boot diagnostics, panic hooks, smoke fixtures). The returned handle is 'static. Never free.
rclc_logger_t for the default ("nros") logger. | void rclc_log_emit | ( | rclc_logger_t | logger, |
| rcl_log_severity_t | severity, | ||
| const char * | message, | ||
| size_t | message_len | ||
| ) |
Low-level emit. Renders message (already-formatted UTF-8 text; NOT null-terminated by contract — pass an explicit length) at severity through the dispatcher.
Most users should use the NROS_LOG_* printf-style macros below rather than calling this directly.
| logger | Logger handle from rclc_node_get_logger. NULL = silently drops (kept total to simplify call sites). |
| severity | One of rcl_log_severity_t. |
| message | UTF-8 text; not required to be null-terminated. |
| message_len | Length of message in bytes. |
| void void rclc_log_emit_at | ( | rclc_logger_t | logger, |
| rcl_log_severity_t | severity, | ||
| const char * | message, | ||
| size_t | message_len, | ||
| const char * | file, | ||
| uint32_t | line | ||
| ) |
phase-417 W4.d — emit WITH the call site.
rclc_log_emit above hardcoded file = "<nros-c>", line = 0 and timestamp_ns = 0 into every record, contradicting this file's own header comment ("attach severity + line info"). Every C and C++ record in the tree therefore reached the sinks stripped of the two fields the sinks render.
| file | Must have STATIC storage duration — __FILE__. Borrowed for the life of the program, never copied. NULL = unknown. |
| line | __LINE__. |
The timestamp is NOT a parameter: it comes from nros_log's own clock, so a C record is stamped by the same clock at the same resolution as a Rust one. See rclc_log_timestamp_available().
| void rclc_log_emit_fmt | ( | rclc_logger_t | logger, |
| rcl_log_severity_t | severity, | ||
| const char * | fmt, | ||
| ... | |||
| ) |
Internal helper used by the macros. Formats fmt + args into a stack buffer, then calls rclc_log_emit. Buffer size = 256 bytes; overflow is truncated + appended ....
| void rclc_log_emit_fmt_at | ( | rclc_logger_t | logger, |
| rcl_log_severity_t | severity, | ||
| const char * | file, | ||
| uint32_t | line, | ||
| const char * | fmt, | ||
| ... | |||
| ) |
phase-417 W4.d — printf-style emit carrying __FILE__ / __LINE__.
Defined beside rclc_log_emit_fmt in c-stubs/log_fmt.c (the variadic entry point cannot be written in Rust: c_variadic is unstable), and rendered there into a NROS_LOG_FMT_BUFFER_SIZE stack buffer. It was a static inline here until phase-492, which made this header include <stdio.h> for vsnprintf; the C API's public headers are freestanding now, so formatting happens in the library, where a libc is linked anyway.
| rcl_log_severity_t rclc_log_get_default_level | ( | void | ) |
The threshold every logger without a level of its own filters on.
RCLC_LOG_SEVERITY_INFO until rclc_log_set_default_level moves it.
| rclc_logger_t rclc_log_get_logger | ( | const char * | name | ) |
Look up a logger by name, CREATING one if the name is new.
rclcpp::get_logger("x")'s shape. Until phase-417 the C surface had only rclc_log_default_logger(), so every C node in an image shared one logger and therefore one threshold.
Total — always returns a usable handle, 'static, never freed. A NULL or empty name, a name longer than 48 bytes, or an exhausted logger arena answers the CATCH-ALL logger and says so at WARN through that logger. Check the log before calling rclc_logger_set_level on a handle you did not verify: the catch-all's threshold is shared with every other unnamed logger.
Arena size is the NROS_LOG_DYNAMIC_LOGGERS knob (default 16; Linux host boards 32).
| name | NUL-terminated UTF-8. Copied; need not outlive the call. |
| void rclc_log_init | ( | void | ) |
Phase 88.16.H — explicit installation of the default sink list.
Cross-language no_std builds (FreeRTOS / NuttX / ThreadX C and C++ examples) must call this once after the board crate's platform-log writer is registered (typically right after rclc_node_init_default returns). Pins nros_log::init as a linker root so --gc-sections keeps it; without this, the lazy guard inside rclc_log_emit can be unreachable and every record silently drops.
Idempotent. Hosted POSIX consumers can skip this — the .init_array ctors already wire the dispatcher.
| void rclc_log_set_default_level | ( | rcl_log_severity_t | severity | ) |
Move the threshold that every logger WITHOUT a level of its own filters on.
phase-467. Retroactive: a logger interned long before this call follows it, because "no level of its own" is a stored state and not a value copied in at construction. A logger that states its own level is NOT overridden.
RCLC_LOG_SEVERITY_UNSET here restores RCLC_LOG_SEVERITY_INFO, which is rcutils_logging_set_default_logger_level's documented behaviour for the same argument. Every other value resolves by band, as everywhere else on this surface.
Not rclc_logger_set_level(rclc_log_default_logger(), ...): that moves the threshold of the one logger NAMED "nros", which is a different logger from "every logger that has not been given a level".
| bool rclc_log_throttle_admit | ( | uint64_t * | last_ns, |
| uint64_t | interval_ms | ||
| ) |
Decide whether a throttled call site may emit now, and arm its window.
The NROS_LOG_*_THROTTLE macros call this; a hand-written call site can too.
| last_ns | Caller-owned storage — a function-scope static uint64_t x = 0;, one per call site. Caller-owned rather than a handle into a pool because a pool needs a bound, and a throttled call site is not a resource anyone should have to budget. OPAQUE: 0 means "never emitted" and every other value is written by this function — read it as a timestamp and you will read it wrong (bit 0 is a flag; the timestamp sits above it, mod 2^63). |
| interval_ms | Minimum gap between emitted records at this site. |
rclcpp). NULL last_ns returns true: a broken caller gets every record, never silence.The rule itself is nros_log::throttle_decide — the same function the Rust nros_*_throttle! macros use. Only the storage differs. A clock that wraps still measures real elapsed time across the wrap; a clock that is STUCK (at any value, 0 included) admits once and then never (issue 1152).
Without a monotonic clock every timestamp is 0, the window can never elapse, and each site emits its first record and nothing after it; the first such call reports that at WARN rather than degrading quietly.
| bool rclc_log_timestamp_available | ( | void | ) |
Whether records carry a real timestamp_ns rather than a constant 0.
False means nros-log was built without the platform clock — neither the platform's [capabilities] clock nor its platform-clock feature (issue 1037) — so nothing derived from the timestamp — the throttle windows above all — has a time base. See rclc_log_throttle_admit.
| rclc_logger_t rclc_logger_get_child | ( | rclc_logger_t | logger, |
| const char * | suffix | ||
| ) |
A logger that is a descendant of logger — rclcpp::Logger::get_child's C half (RFC-0102). Named <parent>.<suffix>; the catch-all's (rclc_log_default_logger()) children are top-level names (suffix).
A child with no level of its own filters at its nearest ancestor's, so rclc_logger_set_level(parent, RCLC_LOG_SEVERITY_DEBUG) reaches it. The same name answers the same handle, 'static, never freed.
rclc_logger_set_level(child, ..) move the parent's threshold. rclcpp::Logger::get_child emits through the parent and refuses set_level in that case.| suffix | NUL-terminated UTF-8. Copied; need not outlive the call. |
| rcl_log_severity_t rclc_logger_get_level | ( | rclc_logger_t | logger | ) |
logger's EFFECTIVE runtime severity threshold — its own level if it has one, the process default if it does not.
rcutils_logging_get_logger_effective_level's answer rather than rcutils_logging_get_logger_level's: this is the number that decides whether a record is emitted, so it is never RCLC_LOG_SEVERITY_UNSET.
A NULL handle answers RCLC_LOG_SEVERITY_FATAL — the quiet end — so a dropped handle cannot read as "trace everything".
| size_t rclc_logger_get_name | ( | rclc_logger_t | logger, |
| char * | buf, | ||
| size_t | buf_len | ||
| ) |
Copy logger's name into buf as a NUL-terminated string.
snprintf's convention, so one call can size a buffer. 0 for a NULL logger. Writes nothing when buf is NULL or buf_len is 0.A copy rather than a borrowed pointer because the Rust side holds a length-delimited &str: its pointer is not NUL-terminated, and handing it out would hand out a string every C function reads past the end of.
| bool rclc_logger_is_enabled | ( | rclc_logger_t | logger, |
| rcl_log_severity_t | severity | ||
| ) |
Whether a record at severity would pass logger's threshold.
The NROS_LOG_*_THROTTLE macros call this so the severity test runs BEFORE the throttle window: a record the level filters must not consume the window. NULL handle answers false.
| bool rclc_logger_set_level | ( | rclc_logger_t | logger, |
| rcl_log_severity_t | severity | ||
| ) |
Set logger's runtime severity threshold. Records below it are dropped before any sink sees them.
RCLC_LOG_SEVERITY_UNSET takes the level away again rather than setting one: the logger goes back to filtering on rclc_log_get_default_level(). See the rcl_log_severity_t comment for what that inheritance is and is not.