ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
log.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file log.h
6 * @brief Abstract logging whose disabled levels cost nothing at all (PROTOCORE_LOG_LEVEL).
7 *
8 * Instrumentation is only worth leaving in the source permanently if a build that does not want it
9 * pays nothing for it - not a branch, not a call, and not a format string sitting in flash. A
10 * runtime `if (level >= threshold)` fails that last part: every message is still linked in, and on
11 * a device flash is the scarce resource.
12 *
13 * So the filter is the preprocessor. A call below PROTOCORE_LOG_LEVEL expands to a form that names its
14 * arguments only inside `sizeof(...)` - an unevaluated context - which emits no code and no string
15 * literal, yet still runs the compiler's printf format checking over them and marks the arguments
16 * used (so a variable read only by a log does not warn). Enable the level and the same line starts
17 * logging, with no source change.
18 *
19 * Where an emitted line goes is the caller's choice: it is handed to server/logbuf's ring when
20 * PROTOCORE_ENABLE_LOGBUF is on, and to a sink callback registered with ::LogNs::set_sink (a serial port,
21 * syslog, a websocket console) if there is one.
22 *
23 * @author Douglas Quigg (dstroy0)
24 * @date 2026
25 */
26
27#ifndef PROTOCORE_LOG_H
28#define PROTOCORE_LOG_H
29
30#include "protocore_config.h"
31
33
34// Only ever pointed at from here, so the tags are enough and the engine's header stays out of
35// every translation unit that includes this one.
36struct protocore_field;
37struct protocore_fval;
38
39/** @brief Receives an emitted line, already formatted. @p level is a PROTOCORE_LOG_LEVEL_* value. */
40typedef void (*protocore_log_sink_fn)(uint8_t level, const char *line);
41
42/**
43 * @brief Declared, never defined: only ever named inside `sizeof`, so no call is ever generated.
44 *
45 * It exists to mark a discarded statement's arguments as used, so a variable read only by a log
46 * does not warn its way into being deleted.
47 */
48int protocore_log_discard_args(const struct protocore_field *spec, const struct protocore_fval *v, size_t nv);
49
50/** @brief The discarded form: marks arguments used, emits nothing. */
51#define PROTOCORE_LOG_DISCARD(spec, v, nv) \
52 do \
53 { \
54 (void)sizeof(protocore_log_discard_args((spec), (v), (nv))); \
55 } while (0)
56
57/** @brief One line to emit: its level, and the spec that shapes it. */
58typedef struct
59{
60 uint8_t level; ///< the severity the line carries
61 const struct protocore_field *spec; ///< the shape of the message
62 const struct protocore_fval *v; ///< the values that fill it
63 size_t nv; ///< how many
65
66/**
67 * @brief The frame logger.
68 *
69 * A caller sets the members a call takes and invokes it through ::Log. The installed sink is behind
70 * @ref internal; the level macros below do the member-set and the call in one statement.
71 *
72 * @var LogNs::frame one line to emit: its level, and the spec that shapes it
73 * @var LogNs::sink the sink an install registers; NULL clears it
74 * @var LogNs::emit build the line and route it to the ring and/or the sink
75 * @var LogNs::set_sink install (or clear) the sink emitted lines are handed to
76 *
77 * A spec, not a format string: the shape of the message is decided when the code is written, so
78 * nothing here parses anything at runtime, and a build whose logs declare no float field links no
79 * float formatter. A build with every level compiled out still carries the handle, so a caller
80 * compiles either way and emit is the no-op.
81 */
87
88/** @brief The operands and the outcome. */
89extern LogVars LogV;
90
91/** @brief The entries. */
92typedef struct
93{
94 void (*const emit)(uint8_t *work);
95 void (*const set_sink)(uint8_t *work);
96} LogNs;
97
98// What the table binds, defined once in the .c and taking one parameter each: everything
99// else an entry needs is an operand in LogV or a region of the borrow at a fixed offset.
100void protocore_log_emit(uint8_t *work);
101void protocore_log_set_sink(uint8_t *work);
102
103// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
104// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
105// `Log.emit(work)` resolves to a named function and becomes a DIRECT call. An extern table
106// leaves the call indirect and the symbol live at every level, -O2 -flto included.
107static const LogNs Log __attribute__((unused)) = {
109 .set_sink = protocore_log_set_sink,
110};
111
112/**
113 * @brief The PROTOCORE_LOG_BORROW bytes this module's state lives in.
114 *
115 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
116 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
117 * walks, so the state lasts the life of the program.
118 *
119 * @return the span.
120 */
121uint8_t *protocore_log_span(void);
122
123/** @brief Set the frame members and emit, as one statement. */
124#define PROTOCORE_LOG_EMIT(lvl, spec_, v_, nv_) \
125 do \
126 { \
127 LogV.frame.level = (lvl); \
128 LogV.frame.spec = (spec_); \
129 LogV.frame.v = (v_); \
130 LogV.frame.nv = (nv_); \
131 Log.emit(protocore_log_span()); \
132 } while (0)
133
134#if PROTOCORE_LOG_LEVEL <= PROTOCORE_LOG_LEVEL_DEBUG
135#define PROTOCORE_LOGD(spec, v, nv) PROTOCORE_LOG_EMIT(PROTOCORE_LOG_LEVEL_DEBUG, (spec), (v), (nv))
136#else
137#define PROTOCORE_LOGD(spec, v, nv) PROTOCORE_LOG_DISCARD((spec), (v), (nv))
138#endif
139
140#if PROTOCORE_LOG_LEVEL <= PROTOCORE_LOG_LEVEL_INFO
141#define PROTOCORE_LOGI(spec, v, nv) PROTOCORE_LOG_EMIT(PROTOCORE_LOG_LEVEL_INFO, (spec), (v), (nv))
142#else
143#define PROTOCORE_LOGI(spec, v, nv) PROTOCORE_LOG_DISCARD((spec), (v), (nv))
144#endif
145
146#if PROTOCORE_LOG_LEVEL <= PROTOCORE_LOG_LEVEL_WARN
147#define PROTOCORE_LOGW(spec, v, nv) PROTOCORE_LOG_EMIT(PROTOCORE_LOG_LEVEL_WARN, (spec), (v), (nv))
148#else
149#define PROTOCORE_LOGW(spec, v, nv) PROTOCORE_LOG_DISCARD((spec), (v), (nv))
150#endif
151
152#if PROTOCORE_LOG_LEVEL <= PROTOCORE_LOG_LEVEL_ERROR
153#define PROTOCORE_LOGE(spec, v, nv) PROTOCORE_LOG_EMIT(PROTOCORE_LOG_LEVEL_ERROR, (spec), (v), (nv))
154#else
155#define PROTOCORE_LOGE(spec, v, nv) PROTOCORE_LOG_DISCARD((spec), (v), (nv))
156#endif
157
159
160#endif // PROTOCORE_LOG_H
int protocore_log_discard_args(const struct protocore_field *spec, const struct protocore_fval *v, size_t nv)
Declared, never defined: only ever named inside sizeof, so no call is ever generated.
void protocore_log_set_sink(uint8_t *work)
void(* protocore_log_sink_fn)(uint8_t level, const char *line)
Receives an emitted line, already formatted. level is a PROTOCORE_LOG_LEVEL_* value.
Definition log.h:40
uint8_t * protocore_log_span(void)
The PROTOCORE_LOG_BORROW bytes this module's state lives in.
void protocore_log_emit(uint8_t *work)
LogVars LogV
The operands and the outcome.
One line to emit: its level, and the spec that shapes it.
Definition log.h:59
const struct protocore_field * spec
the shape of the message
Definition log.h:61
const struct protocore_fval * v
the values that fill it
Definition log.h:62
size_t nv
how many
Definition log.h:63
uint8_t level
the severity the line carries
Definition log.h:60
The entries.
Definition log.h:93
void(*const emit)(uint8_t *work)
Definition log.h:94
Definition log.h:83
LogFrameArgs frame
Definition log.h:84
protocore_log_sink_fn sink
Definition log.h:85
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
#define PROTOCORE_END_DECLS
Definition types.h:97