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.


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.