defmt, but in C
who says you need rust macros?
defmt is a logging library designed not to serialise the formatted string over the link, but instead just the minimal information necessary.
defmt
("de format", short for "deferred formatting") is a highly efficient logging framework that targets resource-constrained devices, like microcontrollers. — Ferrous Systems, "defmt"
Being able to skip the dynamic interpretation of printf()
arguments at runtime also helps lower the overhead of debug printing.
There is no need to know how to format %.2f
as human-readable bytes for the log emitter.
Instead, this is the responsibility of someone else, all you need to do is send the data bytes.
For an example, consider:
uint8_t a = 232, b = 18, c = 55;
uint64_t d = 100040;
printf("The values are %d, %d, %d, %lu\n", a, b, c, d);
An implementation of printf
needs to:
- Iterate through
fmt
until reaching%
(then emitThe values are
) - Read
%d
and then emit the first argument, in base 10 (not easy!) - Iterate through to the next
%
, (then emit,
) - Repeat a few more times.
In contrast, defmt-style loggers - by virtue of not formatting the string - simply can emit the arguments, i.e.:
- Emit a string index e.g.
02 00
(defmt uses 16-bit little-endian) - Emit the first argument
e8
(single byte) - Repeat a few more times.
- Emit the last argument,
c8 86 01 00 00 00 00 00
. (8 bytes)
huh? #
Basically, we can do this:
int main(void) {
DEFMT_DEBUG(".. DONE");
DEFMT_DEBUG("HELLO: {0=i32} {1=u32}", 5, 7);
DEFMT_DEBUG("HELLO: {0=i32} {1=u8} 0x{2=u32:x}", 5, (uint8_t)7, 35);
DEFMT_ERROR("HELLO: {0=u64}", (uint64_t)5);
DEFMT_WARN("Hello \"\\\" World ({0=i8})\n", (int8_t)-1);
return 0;
}
$ ./build/demo | hexdump -C
00000000 02 00 03 00 05 00 00 00 07 00 00 00 04 00 05 00
00000010 00 00 07 23 00 00 00 06 00 05 00 00 00 00 00 00
00000020 00 05 00 ff
00000024
$ ./build/demo | defmt-print -e build/demo
DEBUG .. DONE
DEBUG HELLO: 5 7
DEBUG HELLO: 5 7 0x23
ERROR HELLO: 5
WARN Hello "\" World (-1)
The main advantage lies in not having to encode the long strings themselves.
how does this work? #
Export a symbol with the name of the symbol containing JSON-encoded information about each log.
Here's the symbol table for the .defmt
section.
| VIRT ADDR | OBJECT SIZE | SYMBOL NAME
0000000000000002 0000000000000000 __DEFMT_MARKER_TRACE_START
0000000000000002 0000000000000000 __DEFMT_MARKER_TRACE_END
0000000000000002 0000000000000000 __DEFMT_MARKER_DEBUG_START
0000000000000002 0000000000000001 {"package":"../src/main.c","tag":"defmt_debug","data":".. DONE","disambiguator":"18"}
0000000000000003 0000000000000001 {"package":"../src/main.c","tag":"defmt_debug","data":"HELLO: {0=i32} {1=u32}","disambiguator":"19"}
0000000000000004 0000000000000001 {"package":"../src/main.c","tag":"defmt_debug","data":"HELLO: {0=i32} {1=u8} {2=u32:x}","disambiguator":"20"}
0000000000000005 0000000000000000 __DEFMT_MARKER_DEBUG_END
0000000000000005 0000000000000000 __DEFMT_MARKER_INFO_START
0000000000000005 0000000000000000 __DEFMT_MARKER_INFO_END
0000000000000005 0000000000000000 __DEFMT_MARKER_WARN_START
0000000000000005 0000000000000001 {"package":"../src/main.c","tag":"defmt_warn","data":"Hello \"\\\" World ({0=i8})\n","disambiguator":"22"}
0000000000000006 0000000000000000 __DEFMT_MARKER_WARN_END
0000000000000006 0000000000000000 __DEFMT_MARKER_ERROR_START
0000000000000006 0000000000000001 {"package":"../src/main.c","tag":"defmt_error","data":"HELLO: {0=u64}","disambiguator":"21"}
0000000000000007 0000000000000000 __DEFMT_MARKER_ERROR_END
0000000000000007 0000000000000001 _defmt_version_ = 4
0000000000000008 0000000000000001 _defmt_encoding_ = raw
0000000000000009 0000000000000000 __DEFMT_MARKER_END
no, but really? #
[insert crimes]
The code is available on git here.
Essentially:
-
Make a symbol in the same manner that defmt does:
#define _DEFMT_SYMBOL_INTERN(variable, level, symbol_name) \ do { \ const static uint8_t symbol _DEFMT_EXPORT_SYMBOL_NAME(symbol_name) \ _DEFMT_KEEP _DEFMT_SECTION(level); \ /* Linker script places starting from vaddr 0 in info section <=> ID */ \ *(variable) = (uint16_t)(uintptr_t)(&symbol); \ } while (0)
To do the equivalent of Rusts'
[export_name]
in C, we can use theasm()
attribute on a symbol name. Of course, Clang and GCC differ in their behaviour; Clang escapes spaces for you, whereas GCC does not. See issue #62101 and also a fun bug I discovered where-save-temps
behaviour in Clang differed than without it: #138390. -
For each argument to the log macro, call an appropriate function to encode the output.
For this, we use the preprocessor
_Generic
:#define _DEFMT_LOG_ENCODE_ONE(value) \ _Generic((value), \ uint64_t: _defmt_c_write_uint64_t, \ uint32_t: _defmt_c_write_uint32_t, \ uint16_t: _defmt_c_write_uint16_t, \ uint8_t: _defmt_c_write_uint8_t, \ int64_t: _defmt_c_write_int64_t, \ int32_t: _defmt_c_write_int32_t, \ int16_t: _defmt_c_write_int16_t, \ int8_t: _defmt_c_write_int8_t)(_defmt_write, (value));
_defmt_write
corresponds to the same function that Rust uses; so should be for the most part compatible with mixed Rust and C.
This is of course, delightfully cursed. Just like printf
, if you mess up the
specification of the types in the format-string compared to passed to the macro,
things will subtly break. At least it won't be undefined behaviour, as all the
formatting is done by a decoder elsewhere.
The statement below expands as:
// This
DEFMT_ERROR("HELLO: {0=u64}", (uint64_t)5);
// expands as...
do {
uint16_t id;
do {
const static uint8_t symbol asm(
"{" "\"package\"" ":" "\"/home/julia/code/defmt-for-c/src/main.c\""
"," "\"tag\"" ":" "\"defmt_error\""
"," "\"data\"" ":" "\"HELLO: {0=u64}\""
"," "\"disambiguator\"" ":" "\"21\""
"}") __attribute__((used)) __attribute__((section(".defmt." "error" ".")));
*(&id) = (uint16_t)(uintptr_t)(&symbol);
} while (0);
do {
_defmt_acquire();
_Generic((id),
uint64_t: _defmt_c_write_uint64_t,
uint32_t: _defmt_c_write_uint32_t,
uint16_t: _defmt_c_write_uint16_t,
uint8_t: _defmt_c_write_uint8_t,
int64_t: _defmt_c_write_int64_t,
int32_t: _defmt_c_write_int32_t,
int16_t: _defmt_c_write_int16_t,
int8_t: _defmt_c_write_int8_t)(_defmt_write, (id));
_Generic(((uint64_t)5),
uint64_t: _defmt_c_write_uint64_t,
uint32_t: _defmt_c_write_uint32_t,
uint16_t: _defmt_c_write_uint16_t,
uint8_t: _defmt_c_write_uint8_t,
int64_t: _defmt_c_write_int64_t,
int32_t: _defmt_c_write_int32_t,
int16_t: _defmt_c_write_int16_t,
int8_t: _defmt_c_write_int8_t)(_defmt_write, ((uint64_t)5));
;
_defmt_release();
} while (0);
} while (0);
This (on x86_64) expands to:
; ./src/main.c:21
; DEFMT_ERROR("HELLO: {0=u64}", (uint64_t)5);
4005df: b8 06 00 00 00 movl $0x6, %eax
4005e4: 66 89 45 f8 movw %ax, -0x8(%rbp)
4005e8: e8 c9 fe ff ff callq 0x4004b6 <_defmt_acquire>
4005ed: 0f b7 45 f8 movzwl -0x8(%rbp), %eax
4005f1: 0f b7 c0 movzwl %ax, %eax
4005f4: 89 c6 movl %eax, %esi
4005f6: bf c4 04 40 00 movl $0x4004c4, %edi # imm = 0x4004C4
4005fb: e8 80 fd ff ff callq 0x400380 <_defmt_c_write_uint16_t@plt>
400600: be 05 00 00 00 movl $0x5, %esi
400605: bf c4 04 40 00 movl $0x4004c4, %edi # imm = 0x4004C4
40060a: e8 81 fd ff ff callq 0x400390 <_defmt_c_write_uint64_t@plt>
40060f: e8 a9 fe ff ff callq 0x4004bd <_defmt_release>
It's possible that there's a better way to handle the _defmt_c_write_int32_t
functions -
I ran into the problem where linking this as a library meant that directly calling
_defmt_write
would call the wrong / non-existent function; I didn't spend much time
on this because it's easily fixable.
void _defmt_c_write_int32_t(_defmt_write_fn_t *write_fn, int32_t v) {
write_fn((struct slice){.ptr = (uint8_t *)&v, 4});
}
limitations #
-
As before, the same limitation as
printf()
, except this time not compiler checked :( However, it is possible in theory to do this withprintf
-style strings and using a more reasonable interning format other than JSON, if one wanted to make an incompatible format that made more sense for C-land. -
Heavy use of macros....
-
The
defmt-decoder
tool is unable to determine the source locations of the prints. This isn't so much of an issue - it is, after all, encoded in the symbol name, albeit for whatever reason never used as anything but for disambiguation. The implementation ofdefmt-decoder
at this moment uses DWARF symbols to look forDW_AT_linkage_name
, which neither Clang nor GCC emit forstatic
s (internal linkage).