The Road to defmt-1.0 #888
Replies: 3 comments 8 replies
-
Trench coat aspect 7 ( |
Beta Was this translation helpful? Give feedback.
-
This looks plausible: #890 I am currently able to print the format string at run-time because it's now (optionally) in the value of the variable not the name of the variable. Rendering it shouldn't be hard from here. |
Beta Was this translation helpful? Give feedback.
-
I noted this on the Zephyr discord, but it'd be really nice to consider as part of 1.0 encoding the message identifiers in a way that doesn't constantly change using a hash rather than an indexed integer. I don't have a lot of experience with pigweed but this looked really nice to me as one of the attributes of the pw_tokenize module used in their dictionary logging feature https://pigweed.dev/pw_tokenizer/tokenization.html#token-encoding The identifier to the messages can still be a word in size so I don't think much is lost there. |
Beta Was this translation helpful? Give feedback.
-
The Road to Defmt 1.0
What is defmt?
The
defmt
library is described in thedefmt
book as:It should be noted that according to this intro, it doesn't target microcontrollers exclusively, nor does it target Arm systems in particular. This intro does, however call out high efficiency as a key feature.
Fundamentally,
defmt
is many different things wearing a trenchcoat. It is:An API for libraries to emit logging calls:
These calls are similar to, but not fully compatible with, the
std::fmt
machinery used inprintln!
and friends. In particulardefmt
supports=u32
and similar to specify the type of the value being printed, butstd::fmt
does not. We added this to save space because then the type of the value doesn't need to be serialised into the logging frame.An implementation of that API, contained in the
defmt
crate. This includes macros which emit magically named symbols that intern the format strings and other metadata using (3), then call into the registered globaldefmt
transport using (3) to send messages using (4).A stable ABI for a registered global
defmt
transport to receive messages from logging calls made in libraries (where the library is totally unaware of and decoupled from the chosen logging implementation). This is done using theLogger
trait, and theglobal_logger!
macro.A mechanism for emitting log messages as both interned strings (at compile time) and log frames (at run time). As this mechanism can change over time, it is identified with a Defmt Version value. A Defmt Version is a string, but currently each prior version has had a simple numeric value; the current Defmt Version is
"4"
. This version number unambiguously identifies both the the string interning mechanism (5) and the wire format (6), which then allows thedefmt
logs to be reconstituted.The Defmt Version produced by the
defmt
crate is indicated in the ELF symbol table by a symbol with a name like_defmt_version_ = X
. As ofdefmt-0.3.8
, that symbol is called_defmt_version_ = 4
to indicate Defmt Version 4. Note that it does not indicate the version number of thedefmt
crate. It would in theory be possible for one version of thedefmt
crate to offer multiple Defmt Versions, controlled with feature flags - although it is highly likely that only one could be selected at a time to avoid conflicts.Adding a new Defmt Version is not considered a breaking change, because of this mechanism to self-describe which version it is using, although it may require users to update any host tools (like
probe-rs
) to use a new enough version ofdefmt-decoder
. Dropping support withindefmt-decoder
for an older wire format is considered a breaking change to that crate.A mechanism for embedding
defmt
interned strings and metadata in such a way that they do not occupy space in the target's flash, but can be read by a host-side tool likedefmt-decoder
. In Defmt Version 4, the data is stored as JSON formatted symbol names within an ELF file, such that each is allocated a unique 'memory address' (which acts as the interned string's ID).These symbols are placed within a NOLOAD section and so do not occupy any storage space on the target. Here is an example of a symbol from Defmt Version 4, as viewed with
objdump
:We can see it lies at memory address
0x0000_0002
, thus giving it an ID of2
. The JSON encoded object tells of which package generated the message, the log level, and the string itself.A mechanism for encoding
defmt
log messages intodefmt
log frames (the wire format). Any given Defmt Version may have multiple options for how this encoding is performed, which will be indicated using symbols within the.demft
section, like_defmt_encoding_ = rzcobs
.A trait,
defmt::Format
, which lets user-defined data types present a mechanism by which they can be encoded into the 'wire format'. This then allows them to be passed as arguments to thedefmt
logging macros in (1). There is an associated derive-macro which can automatically implement this trait on anystruct
orenum
comprising only values that implement that trait (in a similar vein toderive(Debug)
forcore::fmt::Debug
).A reference logging transport called
defmt-rtt
which sends encodeddefmt
frames over Segger's Real Time Transport (RTT). RTT is an in-memory ringbuffer which is written to by the target's CPU and read from by a debug probe under the control of a host machine. RTT is usually used to send strings (like a serial port) but we use it to send encodeddefmt
frames.A reference logging parser library called
defmt-parser
, which can readdefmt
log frames (e.g. as received over RTT via a debug probe connected to an MCU that is usingdefmt-rtt
as its logging transport) and turn them into Rust structures.A reference logging decoder library called
defmt-decoder
, which can read structures that representdefmt
log frames (e.g. as output bydefmt-parser
) and turn them into formatting Unicode strings.A reference logging parser binary called
defmt-print
, which usesdefmt-decoder
to decode standard input, and prints formatted log message to standard output.There is a delicate balance between these components. As one example, it is important that a tool that uses
defmt-decoder
(e.g. the popularprobe-rs
programming tool) uses a version that knows about the various different Defmt Versions that it might encounter. In addition, is important that adefmt
transport likedefmt-rtt
is compatible with thedefmt
logging crate being used - even if you are using a driver crate that hasn't been updated for some time and was written to use an older version of thedefmt
library. Because the logging library and the transport communicate through magically named macro-generatedextern "C"
functions, we must limit users to a single instance of thedefmt
crate in the dependency tree. This means major version bumps on thedefmt
crate have ecosystem-splitting effects that we really want to avoid.What does a 1.0 mean?
A 1.0 release is a commitment to stability, as demonstrated to great effect by the Rust Project itself.
When we started,
defmt
was a novel idea and no-one was certain it would work out in practice. The response has been remarkable:defmt
(mostly libraries usingdefmt
to emit logs, but also somedefmt
transports)defmt
(a mixture of libraries and binaries)However, to get there,
defmt
has had to change and adapt. Currently we are ondefmt
v0.3.8, which implements Defmt Version 4, anddefmt-decoder
v0.3.11, which can interpret Defmt Version 4 and Defmt Version 3.The 1.0 release of
defmt
will mark an official end to the churn (although we've been basically doing that for a while anyway), and the beginning of the Knurling project's commitment to stability.A proposal for a defmt 1.0 release
Libraries will be able to specify
defmt = "1"
in theirCargo.toml
file. Subsequent releases ofdefmt
will obey Semantic Versioning. There is currently no intention to ever produce adefmt
2.0 release.There will be a final 0.3-series release of
defmt
, which importsdefmt = "1"
and re-exports adefmt-0.3
compatible API.Any library currently using
defmt = "^0.3"
in theirCargo.toml
file can replace that line withdefmt = "1"
, and everything will continue to work, unless you are locked to a version of0.3
prior to the final 1.0-compatible version.A binary can link against a library using
defmt = "^0.3"
and a different library usingdefmt = "1"
, at the same time (this was/is not possible fordefmt
0.3 anddefmt
0.2), unless you are locked to a version of0.3
prior to the final 1.0-compatible version.The
defmt-decoder
will be updated to 1.0, which will be a breaking change for any binary or library using the existing version. These breaking changes will be documented, with common fixes identified. A PR will be produced forprobe-rs
to update todefmt-decoder
1.0. Subsequent releases ofdefmt-decoder
will obey Semantic Versioning 2.0.0.The ABI between the
defmt
library and the transport implementations (likedefmt-rtt
) will be stabilised as-is and documented in thedefmt
book.The
defmt-rtt
transport will be updated to 1.0, but any application usingdefmt-rtt = "^0.3"
can be updated to usedefmt-rtt = "^1.0"
with no further changes required to the program.The list of Defmt Versions supported by
defmt-decoder
(and hencedefmt-print
) will be documented, including the period of time for which the Knurling Project is guaranteeing to support each of those formats with support and updates indefmt-decoder
.A mechanism will be documented whereby users can add their own custom Defmt Versions. A set of Defmt Versions will be reserved for private use, along the lines of the Unicode Private Use Area. This will allow users to fork the
defmt
crate anddefmt-decoder
crates for private use, confident that the production versions will never start producing output with a conflicting Defmt Version. Once fully developed, there is the option to upstream such changes, but where significant differences are to be found, we might want to optionally enable the new encoding to make upgrades easier for existing users.For example, a user might wish to change the wire format so that every defmt log frame includes a single byte to indicate the logging level, rather than that information only being available inside the interned metadata. They could temporarily fork the
defmt
anddefmt-decoder
crates and prototype these changes using a PUA Defmt Version (e.g. Version 1000). At a later time, this could be added to thedefmt
crate as part of, say Defmt Version 7, but as an optional feature enabled in much the same way that RZCOBS encoding is enabled currently (with a symbol called_defmt_encoding = rzcobs
).The
defmt-parser
library will remain an unstable, internal-only, tool. Thedefmt-decoder
will be the stable mechanism to turn encoded binary data into Unicode text with optional ANSI escape-sequence based formatting.Questions we anticipate might be Frequently Asked (QFA)
How do I use defmt on
std
?The difference between
defmt::info!
andlog::info!
means it's not easy to just import either and have the same format string work. But people want to be able to test their libraries on Windows/macOS/Linux, anddefmt
currently explodes there with a linker error.For example, this format string is legal for both because
Square
implements bothdefmt::Format
andcore::fmt::Debug
:This format string will not with with
log
, becuase it uses=u32
to specify the type of the argument as au32
.Using these type specifiers is popular with defmt, because it makes the encoded frame smaller (because the type information is in the interned metadata and so doesn't have to be included in the encoded frame).
We have some ideas about how to resolve this:
defmt::info!
(etc) calls in place. This also means the Windows/macOS/Linux defmt transport has to include thedefmt-decoder
crate and have some way of grabbing the interned strings using only their address, which might be impossible unless we add a feature flag to disable string interning (as part of some future Defmt Version) or find a way to make the Windows/Linux/macOS linkers less sad about our string interning mechanism.std
feature in thedefmt
crate which makes it export macros with the same API and syntax, but that render the whole line to aString
and then pass it to thelog
crate. Not super efficient, but if you are usinglibstd
you probably aren't as fussed about performance as ano_std
user would be.We need to explore this before committing to a 1.0 release. Volunteers welcome!
Can
defmt::Format
be dyn-compatible?Someone wants to be able to pass
&dyn defmt::Format
but can't because we have a static function in the trait.Unfortunately I cannot see a way to remove it. The serialiser needs to report what kind of thing is being serialised so the deserialiser can unpack it. This is done using an interned string:
If
_format_tag()
takes&self
, then you can't callT::_format_tag()
when serialising a slice, and we can't guarantee that the slice has non-zero length, so we can't callvalues[0]._format_tag()
either. So the trait has to include a static method, and that means we can't have dynamic trait objects.Runtime filtering
Someone is asking for target-side run-time filtering of log messages. In other words, to compile them all in (like with
DEFMT_LOG=trace
) but then control at run-time which modules have logging enabled, and which log levels get emitted. The problem is that the metadata about the log level and the source module goes into the magic symbol, but isn't available todefmt-rtt
on the target at log time.A solution may be possible where
defmt
gains a staticAtomicU8
and makes choices about logging at run-time. Such a feature would be opt-in, but probably wouldn't require a new Defmt Version. This then also means that every transport (likedefmt-rtt
) gains the feature automatically because it happens insidedefmt
before frames are transported.ASCII-safe encoding of frames
Currently we support either raw encoding or RZCOBS encoding of
defmt
frames.We have been asked if (an ASCII compatible one) is possible.
We should be able to add this using a new encoding as part of some future Defmt Version N. The ASCII compatible encoding would be specified using a new symbol, like
_defmt_encoding = base64
.Can I store metadata somewhere other an ELF file's symbols?
It's quite common for people to want to send defmt encoded logs Over The Air, because they are small. But they don't want to have their cloud service parsing ELF files. Instead, they'd like the interned strings stored in another format.
It's likely this could be achieved by writing a tool that processes an symbol table and produces a JSON file containing all the interned strings. We would then stabilise this JSON-encoded representation of the
defmt
metadata, alongside the the symbol-table version.We should do some testing to see how the symbol interning works on macOS (Mach-O) and Windows (Portable Executable) as well.
Why can't filenames begin with digits?
Currently
defmt
cannot handle filenames that being with digits or are otherwise not a valid identifier.This seems like the macro being overly strict. Reducing this restriction should be fine post-1.0, or even pre-1.0.
Beta Was this translation helpful? Give feedback.
All reactions