Design & impl notes
Unstructured, braindump-ish notes about the design and implementation of defmt
WARNING the notes here may not accurately reflect the current implementation. This document is synchronized with the implementation at a best effort basis.
Optimization goals
defmt
optimizes for data throughput first and then for runtime cost.
Constraints
No double compilation
Say you want print logs from target/device app that uses crate foo
.
That crate foo
uses the Format
trait on some of its data structures.
In this scenario we want to avoid having to compile foo
for the host.
In other words, foo
should only be (cross) compiled for the target device.
This is the biggest difference between defmt
and some serde
library that does binary serialization.
The serde
library requires a Deserialize
step that requires compiling foo
for the host (see derive(Serialize)
).
defmt
avoids this by placing all the required information for formatting in a "side table" (see the interning section).
This comes with the downside that the host can only perform limited actions on the data it receives from the device: namely formatting.
The host cannot invoke foo::Struct.method()
for example but that may not even be a sensible operation on the host anyways, e.g. foo::USB::RegisterValue.store_volatile()
would make the host crash.
We want to avoid this "double" compilation (cross compile for the target and compile for the host) because:
- it doubles compilation time (wait time)
- compiling device-specific code for the host can become a headache quickly: see inline/external assembly, build scripts, etc.