feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"files":{".cargo_vcs_info.json":"3ac4f67ca8e41a8cd691947d4bed5c5c981180dc4d8c495b029126e0f17bce56","Cargo.lock":"1a3702967c07dca906eeba3b6c5caf1ca950a7c585b09b5566ffb924a25148a0","Cargo.toml":"5d18f4bf12826186cb4dcc981eee990a40715d0f851e11db488712ca55d59d9a","Cargo.toml.orig":"acd087b64daad1c226f2ecd1ebd60ac53a8a215acebdef30dcd6795dc9cda439","README.md":"592432ee6ec4bc412dca33878b5659eeed55f596a6e0a47c557b8d1c3e7196a3","build.rs":"e95adebcba8785d938de75f1555c0baf2ab7a7bf527c01fb9a5f36380fc4a2aa","src/attributes.rs":"90a5d3493cddb2f020302e80559554e52c150ddfa5a1fd78bebf3bc8456960e0","src/attributes/global_logger.rs":"f040f33e4ed055acea7c355c3b9bd83f664b63879ea68e9b2b2a527bc21bcace","src/attributes/panic_handler.rs":"fdae585c81f3d17bfc7b7a8ee4ed12b097b208f89ba955abfa8ae47a2b807646","src/cargo.rs":"42862e04cdd1cab46061ffb64a8bad5947d29e14dccb8cb53c7b0af5ef9a7bc4","src/construct.rs":"102081c3d84cccc4275cb2b08a581efe91d34bb49f705b7d2ae7b4e48f57aa49","src/construct/symbol.rs":"085e6450a615fd78d75b54b884a184b525811f44601233d0c8e54a53a489dae0","src/consts.rs":"3c39d7e829ff1fb0b8d5c7fd39e5084ab0b88417bafe6baf154a3a55d1f08b97","src/derives.rs":"3cf769348e8e12ecda93bdab2939ed2b104c9d160fdf0db160a126d3f98e2964","src/derives/format.rs":"5c1e5a67d72259268a5bfa687e54b06a4f7e2a82fb0dd81393825ced9cbc3d1e","src/derives/format/codegen.rs":"1fdfed305c37f16d7bcc93c8a1d8059fb29a806595a7411b25c27d63a7eaa561","src/derives/format/codegen/enum_data.rs":"8930b0e36740485c18c208c32cd9d5dabbc5ccc02ddc7b1aa9f0febda61a4b9a","src/derives/format/codegen/fields.rs":"1789482dfe920cfa895d5caf33bd576a2f5c8f80db741a21ec5b3f4c4a56aec5","src/function_like.rs":"c53829b5c90f726f81146cfa7741f24dd9e16219591a93d726a0ee7a25f7a07a","src/function_like/assert_binop.rs":"f6946a78e0fc93c86c19fc11f6d554106ca5eab3f0c4d457313df979d2cd43a4","src/function_like/assert_binop/args.rs":"754045bf1b3f8bb51299f76cee83a3d88b55fed9b57268ad63097b7e6ad7bd35","src/function_like/assert_like.rs":"96b123f1e02eb80eb5e60deea92d7eace1aefac0179fb0e3279e7a4504178fa3","src/function_like/assert_like/assert.rs":"dfcbc594da1877ffc382bdd86dc226d9c664cb543df389eef4c3e86a214d0728","src/function_like/assert_like/unwrap.rs":"93049f262b81a1f8782664922da0a27b812847d28973ca5d370bab7570c009eb","src/function_like/dbg.rs":"e6d9014f112fd54e495be2078bd5399d50bdbee301d83c6c04d254d584782fe9","src/function_like/dbg/args.rs":"ddc9677f6188b2ef7ca7ea068ac0d1e5cee6236c3fc64e05363f90ad64c61bfd","src/function_like/intern.rs":"6f265bec62e83ca75b99b90f4749b62d36dd5310f32ae2fe1447d716c61195ab","src/function_like/internp.rs":"4546356e90c5f059ac3eaeb97b3b824eaeab0bb3f58b83115003fea24c595f9c","src/function_like/log.rs":"d17e2efa264017db013305f8407e20bf5ddd1552317bb15f2d496230c34cf802","src/function_like/log/args.rs":"2c86b3f8d748faf1a2e4067232d60516cd9bd4d24921f4ea5848b0797ce0edfd","src/function_like/log/codegen.rs":"5eea62064a5c964dcb1133a467b93994fd91771e8555a372927be42e525c74c0","src/function_like/log/env_filter.rs":"8c28e1f1b8e65836e8eb6bead575d293e3ad1b40777921af320904bda3d08297","src/function_like/log/env_filter/parse.rs":"1615d093cc80c142c1c4d17ebe3a230e4b0c96143ebb2d15e7c09e055cec1049","src/function_like/panic_like.rs":"3e0b1e2bedccefc0026a8cccc08da6d2d9ef885ca0c9c08fb30c75fa0455d473","src/function_like/println.rs":"8f6583bcffe2ee4654602f6bd38f3940d74840be9cd72c89c1634512027dcd92","src/function_like/write.rs":"80f1bb9d71a22688e5af3e766035ae4b855ff8cfb76bc3f353b73e5cdc512e1c","src/function_like/write/args.rs":"67eca9612845b0e62c8098cf7be08b2ab172953df4c25cdc9cd107d673c0c742","src/items.rs":"ac6068952ec12add139db6d87779e94d1af772374d68a2b03ab6b59fecda22dd","src/items/bitflags.rs":"ac6086a81088fc912d91fb221ba24722598fe64993cc7372370d4c4dd50d6e08","src/items/bitflags/input.rs":"7d4e202ce7e9b60b690831c5f26483910934a3121d0cadfe7cab52d0445f01ad","src/items/timestamp.rs":"9d2f01929641c9880b3c2b65af72dda6fb327adf9a0b131faf6203fea972f3df","src/lib.rs":"bc4544a29520b2f45789b376dbeb60baff47d34b92d984de4949a85792f615e9"},"package":"3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e"}
+7
View File
@@ -0,0 +1,7 @@
{
"git": {
"sha1": "8459657ef6df5e6f3386512744110caef5f0612a",
"dirty": true
},
"path_in_vcs": "macros"
}
+235
View File
@@ -0,0 +1,235 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "defmt-macros"
version = "1.0.1"
dependencies = [
"defmt-parser",
"maplit",
"pretty_assertions",
"proc-macro-error2",
"proc-macro2",
"quote",
"rstest",
"syn",
]
[[package]]
name = "defmt-parser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "rstest"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330"
dependencies = [
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25"
dependencies = [
"cfg-if",
"glob",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "semver"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+67
View File
@@ -0,0 +1,67 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "defmt-macros"
version = "1.0.1"
authors = ["The Knurling-rs developers"]
build = "build.rs"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "defmt macros"
readme = "README.md"
keywords = [
"knurling",
"defmt",
]
license = "MIT OR Apache-2.0"
repository = "https://github.com/knurling-rs/defmt"
[features]
unstable-test = []
[lib]
name = "defmt_macros"
path = "src/lib.rs"
proc-macro = true
[dependencies.defmt-parser]
version = "=1.0.0"
[dependencies.proc-macro-error2]
version = "2"
[dependencies.proc-macro2]
version = "1"
[dependencies.quote]
version = "1"
[dependencies.syn]
version = "2"
features = [
"full",
"extra-traits",
]
[dev-dependencies.maplit]
version = "1"
[dev-dependencies.pretty_assertions]
version = "1"
[dev-dependencies.rstest]
version = "0.19"
default-features = false
+41
View File
@@ -0,0 +1,41 @@
# `defmt-macros`
`defmt` ("de format", short for "deferred formatting") is a highly efficient logging framework that targets resource-constrained devices, like microcontrollers.
For more details about the framework check the book at <https://defmt.ferrous-systems.com>.
This library contains the proc macros used by
[`defmt`](https://crates.io/crates/defmt).
## MSRV
The minimum supported Rust version is 1.76 (or Ferrocene 24.05). `defmt` is tested against the latest stable Rust version and the MSRV.
## Support
`defmt` is part of the [Knurling] project, [Ferrous Systems]' effort at
improving tooling used to develop for embedded systems.
If you think that our work is useful, consider sponsoring it via [GitHub
Sponsors].
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE](../LICENSE-APACHE) or
<http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](../LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
licensed as above, without any additional terms or conditions.
[Knurling]: https://knurling.ferrous-systems.com/
[Ferrous Systems]: https://ferrous-systems.com/
[GitHub Sponsors]: https://github.com/sponsors/knurling-rs
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("cargo:rerun-if-env-changed=DEFMT_LOG");
}
+4
View File
@@ -0,0 +1,4 @@
//! Procedural macros that are attributes
pub(crate) mod global_logger;
pub(crate) mod panic_handler;
+66
View File
@@ -0,0 +1,66 @@
use proc_macro::TokenStream;
use proc_macro_error2::{abort, abort_call_site};
use quote::quote;
use syn::{parse_macro_input, Fields, ItemStruct};
pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream {
if !args.is_empty() {
abort_call_site!("`#[global_logger]` attribute takes no arguments")
}
let strukt = parse_macro_input!(item as ItemStruct);
validate(&strukt);
codegen(&strukt)
}
fn validate(strukt: &ItemStruct) {
let is_unit_struct = matches!(strukt.fields, Fields::Unit);
if !strukt.generics.params.is_empty()
|| strukt.generics.where_clause.is_some()
|| !is_unit_struct
{
abort!(
strukt,
"struct must be a non-generic unit struct (e.g. `struct S;`)"
);
}
}
fn codegen(strukt: &ItemStruct) -> TokenStream {
let attrs = &strukt.attrs;
let ident = &strukt.ident;
let vis = &strukt.vis;
quote!(
#(#attrs)*
#vis struct #ident;
#[inline(never)]
#[no_mangle]
unsafe fn _defmt_acquire() {
<#ident as defmt::Logger>::acquire()
}
#[inline(never)]
#[no_mangle]
unsafe fn _defmt_flush() {
<#ident as defmt::Logger>::flush()
}
#[inline(never)]
#[no_mangle]
unsafe fn _defmt_release() {
<#ident as defmt::Logger>::release()
}
#[inline(never)]
#[no_mangle]
unsafe fn _defmt_write(bytes: &[u8]) {
<#ident as defmt::Logger>::write(bytes)
}
)
.into()
}
+78
View File
@@ -0,0 +1,78 @@
use proc_macro::TokenStream;
use proc_macro_error2::{abort, abort_call_site};
use quote::quote;
use syn::{parse_macro_input, Attribute, ItemFn, ReturnType, Type};
pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream {
if !args.is_empty() {
abort_call_site!("`#[defmt::panic_handler]` attribute takes no arguments");
}
let fun = parse_macro_input!(item as ItemFn);
validate(&fun);
codegen(&fun)
}
fn validate(fun: &ItemFn) {
let is_divergent = match &fun.sig.output {
ReturnType::Default => false,
ReturnType::Type(_, ty) => matches!(&**ty, Type::Never(_)),
};
if fun.sig.constness.is_some()
|| fun.sig.asyncness.is_some()
|| fun.sig.unsafety.is_some()
|| fun.sig.abi.is_some()
|| !fun.sig.generics.params.is_empty()
|| fun.sig.generics.where_clause.is_some()
|| fun.sig.variadic.is_some()
|| !fun.sig.inputs.is_empty()
|| !is_divergent
{
abort!(fun.sig.ident, "function must have signature `fn() -> !`");
}
check_for_attribute_conflicts("panic_handler", &fun.attrs, &["export_name", "no_mangle"]);
}
/// Checks if any attribute in `attrs_to_check` is in `reject_list` and returns a compiler error if there's a match
///
/// The compiler error will indicate that the attribute conflicts with `attr_name`
fn check_for_attribute_conflicts(
attr_name: &str,
attrs_to_check: &[Attribute],
reject_list: &[&str],
) {
for attr in attrs_to_check {
if let Some(ident) = attr.path().get_ident() {
let ident = ident.to_string();
if reject_list.contains(&ident.as_str()) {
abort!(
attr,
"`#[{}]` attribute cannot be used together with `#[{}]`",
attr_name,
ident
)
}
}
}
}
fn codegen(fun: &ItemFn) -> TokenStream {
let attrs = &fun.attrs;
let block = &fun.block;
let ident = &fun.sig.ident;
quote!(
#(#attrs)*
#[export_name = "_defmt_panic"]
#[inline(never)]
fn #ident() -> ! {
#block
}
)
.into()
}
+9
View File
@@ -0,0 +1,9 @@
use std::env;
pub(crate) fn package_name() -> String {
env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "<unknown>".to_string())
}
pub(crate) fn crate_name() -> String {
env::var("CARGO_CRATE_NAME").unwrap_or_else(|_| "<unknown>".to_string())
}
+109
View File
@@ -0,0 +1,109 @@
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash as _, Hasher as _},
};
use proc_macro::Span;
use proc_macro2::{Ident as Ident2, Span as Span2, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use syn::{parse_quote, Expr, Ident, LitStr};
pub(crate) use symbol::mangled as mangled_symbol_name;
mod symbol;
pub(crate) fn crate_local_disambiguator() -> u64 {
// We want a deterministic, but unique-per-macro-invocation identifier. For that we
// hash the call site `Span`'s debug representation, which contains a counter that
// should disambiguate macro invocations within a crate.
hash(&format!("{:?}", Span::call_site()))
}
pub(crate) fn escaped_expr_string(expr: &Expr) -> String {
quote!(#expr)
.to_string()
.replace('{', "{{")
.replace('}', "}}")
}
pub(crate) fn interned_string(
string: &str,
tag: &str,
is_log_statement: bool,
prefix: Option<&str>,
defmt_path: &syn::Path,
) -> TokenStream2 {
// NOTE we rely on this variable name when extracting file location information from the DWARF
// without it we have no other mean to differentiate static variables produced by `info!` vs
// produced by `intern!` (or `internp`)
let var_name = if is_log_statement {
format_ident!("DEFMT_LOG_STATEMENT")
} else {
format_ident!("S")
};
let var_addr = if cfg!(feature = "unstable-test") {
quote!({ #defmt_path::export::fetch_add_string_index() })
} else {
let var_item = static_variable(&var_name, string, tag, prefix);
quote!({
#var_item
&#var_name as *const u8 as u16
})
};
quote!({
#defmt_path::export::make_istr(#var_addr)
})
}
/// work around restrictions on length and allowed characters imposed by macos linker
/// returns (note the comma character for macos):
/// under macos: ".defmt," + 16 character hex digest of symbol's hash
/// otherwise: ".defmt." + prefix + symbol
pub(crate) fn linker_section(for_macos: bool, prefix: Option<&str>, symbol: &str) -> String {
let mut sub_section = if let Some(prefix) = prefix {
format!(".{prefix}.{symbol}")
} else {
format!(".{symbol}")
};
if for_macos {
sub_section = format!(",{:x}", hash(&sub_section));
}
format!(".defmt{sub_section}")
}
pub(crate) fn static_variable(
name: &Ident2,
data: &str,
tag: &str,
prefix: Option<&str>,
) -> TokenStream2 {
let sym_name = mangled_symbol_name(tag, data);
let section = linker_section(false, prefix, &sym_name);
let section_for_macos = linker_section(true, prefix, &sym_name);
quote!(
#[cfg_attr(target_os = "macos", link_section = #section_for_macos)]
#[cfg_attr(not(target_os = "macos"), link_section = #section)]
#[export_name = #sym_name]
static #name: u8 = 0;
)
}
pub(crate) fn string_literal(content: &str) -> LitStr {
LitStr::new(content, Span2::call_site())
}
pub(crate) fn variable(name: &str) -> Expr {
let ident = Ident::new(name, Span2::call_site());
parse_quote!(#ident)
}
fn hash(string: &str) -> u64 {
let mut hasher = DefaultHasher::new();
string.hash(&mut hasher);
hasher.finish()
}
+78
View File
@@ -0,0 +1,78 @@
use std::fmt::Write;
use crate::cargo;
pub(crate) fn mangled(defmt_tag: &str, data: &str) -> String {
Symbol::new(defmt_tag, data).mangle()
}
struct Symbol<'a> {
/// Name of the Cargo package in which the symbol is being instantiated. Used for avoiding
/// symbol name collisions.
package: String,
/// Unique identifier that disambiguates otherwise equivalent invocations in the same crate.
disambiguator: u64,
/// Symbol categorization. Known values:
/// * `defmt_prim` for primitive formatting strings that are placed at the start of the `.defmt`
/// section.
/// * `defmt_fmt`, `defmt_str` for interned format strings and string literals.
/// * `defmt_println` for logging messages that are always displayed.
/// * `defmt_trace`, `defmt_debug`, `defmt_info`, `defmt_warn`, `defmt_error` for logging
/// messages used at the different log levels.
/// * `defmt_bitflags` indicates that a format string was generated by a `defmt::bitflags!`
/// invocation, and that the decoder should look up possible flags in the binary.
/// The data string is of the format `NAME@REPR#NUM`, where `NAME` is the name of the bitflags
/// struct, `REPR` is the raw integer type representing the bitflags value (and also the
/// wire format), and `NUM` is the number of defined bitflag values.
/// * `defmt_bitflags_value` marks a `static` that holds the value of a bitflags `const`, its
/// data field is `STRUCT_NAME::FLAG_NAME`.
/// * Anything starting with `defmt_` is reserved for use by defmt, other prefixes are free for
/// use by third-party apps (but they all should use a prefix!).
tag: String,
/// Symbol data for use by the host tooling. Interpretation depends on `tag`.
data: &'a str,
/// Crate name obtained via CARGO_CRATE_NAME (added since a Cargo package can contain many crates).
crate_name: String,
}
impl<'a> Symbol<'a> {
fn new(tag: &'a str, data: &'a str) -> Self {
Self {
// `CARGO_PKG_NAME` is set to the invoking package's name.
package: cargo::package_name(),
disambiguator: super::crate_local_disambiguator(),
tag: format!("defmt_{tag}"),
data,
crate_name: cargo::crate_name(),
}
}
fn mangle(&self) -> String {
format!(
r#"{{"package":"{}","tag":"{}","data":"{}","disambiguator":"{}","crate_name":"{}"}}"#,
json_escape(&self.package),
json_escape(&self.tag),
json_escape(self.data),
self.disambiguator,
json_escape(&self.crate_name),
)
}
}
fn json_escape(string: &str) -> String {
let mut escaped = String::new();
for c in string.chars() {
match c {
'\\' => escaped.push_str("\\\\"),
'\"' => escaped.push_str("\\\""),
'\n' => escaped.push_str("\\n"),
c if c.is_control() || c == '@' => write!(escaped, "\\u{:04x}", c as u32).unwrap(),
c => escaped.push(c),
}
}
escaped
}
+2
View File
@@ -0,0 +1,2 @@
/// The (erased) "Format" type as it appears in `defmt` format strings
pub(crate) const TYPE_FORMAT: &str = "?";
+3
View File
@@ -0,0 +1,3 @@
//! Procedural macros that are `#[derive(*)]` attributes
pub(crate) mod format;
+81
View File
@@ -0,0 +1,81 @@
use proc_macro::TokenStream;
use proc_macro_error2::{abort, abort_call_site};
use quote::{quote, ToTokens};
use syn::{parse_macro_input, parse_quote, Data, DeriveInput};
mod codegen;
pub(crate) fn expand(input: TokenStream) -> TokenStream {
let DeriveInput {
attrs,
vis: _,
ident,
mut generics,
data,
} = parse_macro_input!(input as DeriveInput);
let defmt_path = match defmt_crate_path(&attrs) {
Ok(defmt_path) => defmt_path,
Err(e) => abort!(e),
};
let encode_data = match &data {
Data::Enum(data) => codegen::encode_enum_data(&ident, data, &defmt_path),
Data::Struct(data) => codegen::encode_struct_data(&ident, data, &defmt_path),
Data::Union(_) => abort_call_site!("`#[derive(Format)]` does not support unions"),
};
let codegen::EncodeData {
format_tag,
stmts,
where_predicates,
} = match encode_data {
Ok(data) => data,
Err(e) => return e.into_compile_error().into(),
};
let codegen::Generics {
impl_generics,
type_generics,
where_clause,
} = codegen::Generics::codegen(&mut generics, where_predicates);
quote!(
#[automatically_derived]
impl #impl_generics #defmt_path::Format for #ident #type_generics #where_clause {
fn format(&self, f: #defmt_path::Formatter) {
use #defmt_path as defmt;
#defmt_path::unreachable!()
}
fn _format_tag() -> #defmt_path::Str {
#format_tag
}
fn _format_data(&self) {
#(#stmts)*
}
}
)
.into()
}
fn defmt_crate_path(attrs: &[syn::Attribute]) -> Result<syn::Path, syn::Error> {
let mut defmt_path = parse_quote!(defmt);
let res = attrs
.iter()
.filter(|attr| attr.path().is_ident("defmt"))
.try_for_each(|attr| {
attr.parse_nested_meta(|meta| {
if meta.path.get_ident().is_some_and(|ident| ident == "crate") {
meta.input.parse::<syn::Token![=]>()?;
defmt_path = meta.input.parse::<syn::Path>()?;
Ok(())
} else {
let path = meta.path.to_token_stream().to_string().replace(' ', "");
Err(meta.error(format_args!("unknown defmt attribute `{path}`")))
}
})
});
res.map(|()| defmt_path)
}
+71
View File
@@ -0,0 +1,71 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{DataStruct, Ident, ImplGenerics, TypeGenerics, WhereClause, WherePredicate};
pub(crate) use enum_data::encode as encode_enum_data;
use crate::construct;
mod enum_data;
mod fields;
pub(crate) struct EncodeData {
pub(crate) format_tag: TokenStream2,
pub(crate) stmts: Vec<TokenStream2>,
pub(crate) where_predicates: Vec<WherePredicate>,
}
pub(crate) fn encode_struct_data(
ident: &Ident,
data: &DataStruct,
defmt_path: &syn::Path,
) -> syn::Result<EncodeData> {
let mut format_string = ident.to_string();
let mut stmts = vec![];
let mut field_patterns = vec![];
let (encode_fields_stmts, where_predicates) = fields::codegen(
&data.fields,
&mut format_string,
&mut field_patterns,
defmt_path,
)?;
stmts.push(quote!(match self {
Self { #(#field_patterns),* } => {
#(#encode_fields_stmts;)*
}
}));
let format_tag = construct::interned_string(&format_string, "derived", false, None, defmt_path);
Ok(EncodeData {
format_tag,
stmts,
where_predicates,
})
}
pub(crate) struct Generics<'a> {
pub(crate) impl_generics: ImplGenerics<'a>,
pub(crate) type_generics: TypeGenerics<'a>,
pub(crate) where_clause: WhereClause,
}
impl<'a> Generics<'a> {
pub(crate) fn codegen(
generics: &'a mut syn::Generics,
where_predicates: Vec<WherePredicate>,
) -> Self {
let mut where_clause = generics.make_where_clause().clone();
let (impl_generics, type_generics, _) = generics.split_for_impl();
// Extend where-clause with `Format` bounds for all field types.
where_clause.predicates.extend(where_predicates);
Self {
impl_generics,
type_generics,
where_clause,
}
}
}
@@ -0,0 +1,125 @@
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error2::abort_call_site;
use quote::quote;
use syn::{DataEnum, Ident};
use crate::construct;
use super::EncodeData;
pub(crate) fn encode(
ident: &Ident,
data: &DataEnum,
defmt_path: &syn::Path,
) -> syn::Result<EncodeData> {
if data.variants.is_empty() {
return Ok(EncodeData {
stmts: vec![quote!(match *self {})],
format_tag: construct::interned_string("!", "derived", false, None, defmt_path),
where_predicates: vec![],
});
}
let mut format_string = String::new();
let mut where_predicates = vec![];
let mut match_arms = vec![];
let mut is_first_variant = true;
let discriminant_encoder = DiscriminantEncoder::new(data.variants.len());
let enum_ident = ident;
for (index, variant) in data.variants.iter().enumerate() {
let variant_ident = &variant.ident;
if is_first_variant {
is_first_variant = false;
} else {
format_string.push('|');
}
format_string.push_str(&variant_ident.to_string());
let mut field_patterns = vec![];
let (encode_fields_stmts, encode_field_where_predicates) = super::fields::codegen(
&variant.fields,
&mut format_string,
&mut field_patterns,
defmt_path,
)?;
where_predicates.extend(encode_field_where_predicates.into_iter());
let pattern = quote!( { #(#field_patterns),* } );
let encode_discriminant_stmt = discriminant_encoder.encode(index, defmt_path);
match_arms.push(quote!(
#enum_ident::#variant_ident #pattern => {
#encode_discriminant_stmt
#(#encode_fields_stmts;)*
}
))
}
let format_tag = construct::interned_string(&format_string, "derived", false, None, defmt_path);
let stmts = vec![quote!(match self {
#(#match_arms)*
})];
where_predicates.dedup_by(|a, b| a == b);
Ok(EncodeData {
format_tag,
stmts,
where_predicates,
})
}
enum DiscriminantEncoder {
Nop,
U8,
U16,
U32,
U64,
}
impl DiscriminantEncoder {
fn new(number_of_variants: usize) -> Self {
if number_of_variants == 1 {
Self::Nop
} else if number_of_variants <= usize::from(u8::MAX) {
Self::U8
} else if number_of_variants <= usize::from(u16::MAX) {
Self::U16
} else if number_of_variants as u128 > u128::from(u64::MAX) {
// unreachable on existing hardware?
abort_call_site!(
"`#[derive(Format)]` does not support enums with more than {} variants",
number_of_variants
)
} else if number_of_variants as u64 <= u64::from(u32::MAX) {
Self::U32
} else {
Self::U64
}
}
// NOTE this assumes `index` < `number_of_variants` used to construct `self`
fn encode(&self, index: usize, defmt_path: &syn::Path) -> TokenStream2 {
match self {
// For single-variant enums, there is no need to encode the discriminant.
DiscriminantEncoder::Nop => quote!(),
DiscriminantEncoder::U8 => {
let index = index as u8;
quote!(#defmt_path::export::u8(&#index);)
}
DiscriminantEncoder::U16 => {
let index = index as u16;
quote!(#defmt_path::export::u16(&#index);)
}
DiscriminantEncoder::U32 => {
let index = index as u32;
quote!(#defmt_path::export::u32(&#index);)
}
DiscriminantEncoder::U64 => {
let index = index as u64;
quote!(#defmt_path::export::u64(&#index);)
}
}
}
}
+169
View File
@@ -0,0 +1,169 @@
use std::fmt::Write as _;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{parse_quote, Field, Fields, Index, Type, WherePredicate};
use crate::consts;
pub(crate) fn codegen(
fields: &Fields,
format_string: &mut String,
patterns: &mut Vec<TokenStream2>,
defmt_path: &syn::Path,
) -> syn::Result<(Vec<TokenStream2>, Vec<WherePredicate>)> {
let (fields, fields_are_named) = match fields {
Fields::Named(named) => (&named.named, true),
Fields::Unit => return Ok((vec![], vec![])),
Fields::Unnamed(unnamed) => (&unnamed.unnamed, false),
};
if fields.is_empty() {
return Ok((vec![], vec![]));
}
if fields_are_named {
format_string.push_str(" {{ ");
} else {
format_string.push('(');
}
let mut stmts = vec![];
let mut where_predicates = vec![];
let mut is_first = true;
for (index, field) in fields.iter().enumerate() {
if is_first {
is_first = false;
} else {
format_string.push_str(", ");
}
let format_opt = get_defmt_format_option(field)?;
// Find out if the field type is natively supported by defmt. `ty` will be None if not.
let ty = as_native_type(&field.ty);
// `field_ty` will be the field's type if it is not natively supported by defmt
let field_ty = if ty.is_none() { Some(&field.ty) } else { None };
// Get the field format specifier. Either the native specifier or '?'.
let ty = ty.unwrap_or_else(|| consts::TYPE_FORMAT.to_string());
let ident = field
.ident
.clone()
.unwrap_or_else(|| format_ident!("arg{}", index));
// Find the required trait bounds for the field and add the formatting statement depending on the field type and the formatting options
let bound: Option<syn::Path> = if let Some(FormatOption::Debug2Format) = format_opt {
stmts.push(quote!(#defmt_path::export::fmt(&#defmt_path::Debug2Format(&#ident))));
field_ty.map(|_| parse_quote!(::core::fmt::Debug))
} else if let Some(FormatOption::Display2Format) = format_opt {
stmts.push(quote!(#defmt_path::export::fmt(&#defmt_path::Display2Format(&#ident))));
field_ty.map(|_| parse_quote!(::core::fmt::Display))
} else if ty == consts::TYPE_FORMAT {
stmts.push(quote!(#defmt_path::export::fmt(#ident)));
field_ty.map(|_| parse_quote!(#defmt_path::Format))
} else {
let method = format_ident!("{}", ty);
stmts.push(quote!(#defmt_path::export::#method(#ident)));
field_ty.map(|_| parse_quote!(#defmt_path::Format))
};
if let Some(bound) = bound {
where_predicates.push(parse_quote!(#field_ty: #bound));
}
if field.ident.is_some() {
// Named field.
write!(format_string, "{ident}: {{={ty}:?}}").ok();
patterns.push(quote!( #ident ));
} else {
// Unnamed (tuple) field.
write!(format_string, "{{={ty}}}").ok();
let index = Index::from(index);
patterns.push(quote!( #index: #ident ));
}
}
if fields_are_named {
format_string.push_str(" }}");
} else {
format_string.push(')');
}
Ok((stmts, where_predicates))
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum FormatOption {
Debug2Format,
Display2Format,
}
/// If the field has a valid defmt attribute (e.g. `#[defmt(Debug2Format)]`), returns `Ok(Some(FormatOption))`.
/// Returns `Err` if we can't parse a valid defmt attribute.
/// Returns `Ok(None)` if there are no `defmt` attributes on the field.
fn get_defmt_format_option(field: &Field) -> syn::Result<Option<FormatOption>> {
let mut format_option = None;
for attr in &field.attrs {
if attr.path().is_ident("defmt") {
if format_option.is_some() {
return Err(syn::Error::new_spanned(
field,
"multiple `defmt` attributes not supported",
));
}
let mut parsed_format = None;
attr.parse_nested_meta(|meta| {
// #[defmt(Debug2Format)]
if meta.path.is_ident("Debug2Format") {
parsed_format = Some(FormatOption::Debug2Format);
return Ok(());
}
// #[defmt(Display2Format)]
if meta.path.is_ident("Display2Format") {
parsed_format = Some(FormatOption::Display2Format);
return Ok(());
}
Err(meta.error("expected `Debug2Format` or `Display2Format`"))
})?;
if parsed_format.is_none() {
return Err(syn::Error::new_spanned(
&attr.meta,
"expected 1 attribute argument",
));
}
format_option = parsed_format;
}
}
Ok(format_option)
}
/// Returns `Some` if `ty` refers to a builtin Rust type that has native support from defmt and does
/// not have to go through the `Format` trait.
///
/// This should return `Some` for all types that can be used as `{=TYPE}`.
///
/// Note: This is technically incorrect, since builtin types can be shadowed. However the efficiency
/// gains are too big to pass up, so we expect user code to not do that.
fn as_native_type(ty: &Type) -> Option<String> {
match ty {
Type::Path(path) => {
let ident = path.path.get_ident()?;
let ty_name = ident.to_string();
match &*ty_name {
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
| "i128" | "isize" | "f32" | "f64" | "bool" | "str" => Some(ty_name),
_ => None,
}
}
Type::Reference(ty_ref) => as_native_type(&ty_ref.elem),
_ => None,
}
}
+9
View File
@@ -0,0 +1,9 @@
pub(crate) mod assert_binop;
pub(crate) mod assert_like;
pub(crate) mod dbg;
pub(crate) mod intern;
pub(crate) mod internp;
pub(crate) mod log;
pub(crate) mod panic_like;
pub(crate) mod println;
pub(crate) mod write;
+90
View File
@@ -0,0 +1,90 @@
use defmt_parser::Level;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, punctuated::Punctuated};
use crate::{construct, function_like::log};
use self::args::Args;
mod args;
pub(crate) fn eq(args: TokenStream) -> TokenStream {
expand(args, BinOp::Eq)
}
pub(crate) fn ne(args: TokenStream) -> TokenStream {
expand(args, BinOp::Ne)
}
fn expand(args: TokenStream, binop: BinOp) -> TokenStream {
let args = parse_macro_input!(args as Args);
let left = args.left;
let right = args.right;
let mut formatting_args = Punctuated::new();
let extra_string = if let Some(log_args) = args.log_args {
if let Some(args) = log_args.formatting_args {
formatting_args.extend(args);
}
format!(": {}", log_args.format_string.value())
} else {
String::new()
};
let vals = match binop {
BinOp::Eq => &["left_val", "right_val"][..],
BinOp::Ne => &["left_val"][..],
};
for val in vals {
formatting_args.push(construct::variable(val));
}
let panic_msg = match binop {
BinOp::Eq => format!(
"panicked at 'assertion failed: `(left == right)`{}'
left: `{{:?}}`
right: `{{:?}}`",
extra_string
),
BinOp::Ne => format!(
"panicked at 'assertion failed: `(left != right)`{}'
left/right: `{{:?}}`",
extra_string
),
};
let log_args = log::Args {
format_string: construct::string_literal(&panic_msg),
formatting_args: Some(formatting_args),
};
let log_stmt = log::expand_parsed(Level::Error, log_args);
let mut cond = quote!(*left_val == *right_val);
if binop == BinOp::Eq {
cond = quote!(!(#cond));
}
quote!(
// evaluate arguments first
match (&(#left), &(#right)) {
(left_val, right_val) => {
// following `core::assert_eq!`
if #cond {
#log_stmt;
defmt::export::panic()
}
}
}
)
.into()
}
#[derive(PartialEq)]
enum BinOp {
Eq,
Ne,
}
@@ -0,0 +1,47 @@
use syn::{
parse::{self, Parse, ParseStream},
Expr, Token,
};
use crate::function_like::log;
pub(crate) struct Args {
pub(crate) left: Expr,
pub(crate) right: Expr,
pub(crate) log_args: Option<log::Args>,
}
impl Parse for Args {
fn parse(input: ParseStream) -> parse::Result<Self> {
let left = input.parse()?;
let _comma: Token![,] = input.parse()?;
let right = input.parse()?;
if input.is_empty() {
// assert_eq!(a, b)
return Ok(Args {
left,
right,
log_args: None,
});
}
let _comma: Token![,] = input.parse()?;
if input.is_empty() {
// assert_eq!(a, b,)
Ok(Args {
left,
right,
log_args: None,
})
} else {
// assert_eq!(a, b, "c", d)
Ok(Args {
left,
right,
log_args: Some(input.parse()?),
})
}
}
}
+43
View File
@@ -0,0 +1,43 @@
use syn::{
parse::{self, Parse, ParseStream},
Expr, Token,
};
use super::log;
pub(crate) mod assert;
pub(crate) mod unwrap;
struct Args {
condition: Expr,
log_args: Option<log::Args>,
}
impl Parse for Args {
fn parse(input: ParseStream) -> parse::Result<Self> {
let condition = input.parse()?;
if input.is_empty() {
// assert!(a)
return Ok(Args {
log_args: None,
condition,
});
}
let _comma: Token![,] = input.parse()?;
if input.is_empty() {
// assert!(a,)
Ok(Args {
log_args: None,
condition,
})
} else {
// assert!(a, "b", c)
Ok(Args {
log_args: Some(input.parse()?),
condition,
})
}
}
}
@@ -0,0 +1,39 @@
use defmt_parser::Level;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
use crate::{construct, function_like::log};
pub(crate) fn expand(args: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as super::Args);
let condition = args.condition;
let (format_string, formatting_args) = if let Some(log_args) = args.log_args {
let format_string = format!("panicked at '{}'", log_args.format_string.value());
(format_string, log_args.formatting_args)
} else {
let format_string = format!(
"panicked at 'assertion failed: {}'",
construct::escaped_expr_string(&condition)
);
(format_string, None)
};
let format_string = construct::string_literal(&format_string);
let log_stmt = log::expand_parsed(
Level::Error,
log::Args {
format_string,
formatting_args,
},
);
quote!(
if !(#condition) {
#log_stmt;
defmt::export::panic()
}
)
.into()
}
@@ -0,0 +1,46 @@
use defmt_parser::Level;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, punctuated::Punctuated};
use crate::{construct, function_like::log};
pub(crate) fn expand(args: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as super::Args);
let condition = args.condition;
let (format_string, formatting_args) = if let Some(log_args) = args.log_args {
let format_string = format!("panicked at '{}'", log_args.format_string.value());
(format_string, log_args.formatting_args)
} else {
let format_string = format!(
"panicked at 'unwrap failed: {}'\nerror: `{{:?}}`",
construct::escaped_expr_string(&condition)
);
let mut formatting_args = Punctuated::new();
formatting_args.push(construct::variable("_unwrap_err"));
(format_string, Some(formatting_args))
};
let format_string = construct::string_literal(&format_string);
let log_stmt = log::expand_parsed(
Level::Error,
log::Args {
format_string,
formatting_args,
},
);
quote!(
match defmt::export::into_result(#condition) {
::core::result::Result::Ok(res) => res,
::core::result::Result::Err(_unwrap_err) => {
#log_stmt;
defmt::export::panic()
}
}
)
.into()
}
+40
View File
@@ -0,0 +1,40 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
use crate::construct;
use self::args::Args;
mod args;
pub(crate) fn expand(args: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as Args);
codegen(&args)
}
fn codegen(args: &Args) -> TokenStream {
let tuple_exprs = args
.exprs
.iter()
.map(|expr| {
let escaped_expr = construct::escaped_expr_string(expr);
let format_string = format!("{escaped_expr} = {{}}");
quote!(match #expr {
tmp => {
defmt::trace!(#format_string, tmp);
tmp
}
})
})
.collect::<Vec<_>>();
if tuple_exprs.is_empty() {
// for compatibility with `std::dbg!` we also emit a TRACE log in this case
quote!(defmt::trace!(""))
} else {
quote![ (#(#tuple_exprs),*) ]
}
.into()
}
+17
View File
@@ -0,0 +1,17 @@
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
Expr, Token,
};
pub(crate) struct Args {
pub(crate) exprs: Punctuated<Expr, Token![,]>,
}
impl Parse for Args {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
Ok(Self {
exprs: Punctuated::parse_terminated(input)?,
})
}
}
+9
View File
@@ -0,0 +1,9 @@
use proc_macro::TokenStream;
use syn::{parse_macro_input, parse_quote, LitStr};
use crate::construct;
pub(crate) fn expand(args: TokenStream) -> TokenStream {
let literal = parse_macro_input!(args as LitStr);
construct::interned_string(&literal.value(), "str", false, None, &parse_quote!(defmt)).into()
}
+31
View File
@@ -0,0 +1,31 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, LitStr};
use crate::construct;
pub(crate) fn expand(args: TokenStream) -> TokenStream {
let literal = parse_macro_input!(args as LitStr);
let sym_name = construct::mangled_symbol_name("prim", &literal.value());
let prefix = Some("prim");
let section = construct::linker_section(false, prefix, &sym_name);
let section_for_macos = construct::linker_section(true, prefix, &sym_name);
let var_addr = if cfg!(feature = "unstable-test") {
quote!({ defmt::export::fetch_add_string_index() as u16 })
} else {
quote!({
#[cfg_attr(target_os = "macos", link_section = #section_for_macos)]
#[cfg_attr(not(target_os = "macos"), link_section = #section)]
#[export_name = #sym_name]
static S: u8 = 0;
&S as *const u8 as u16
})
};
quote!({
defmt::export::make_istr(#var_addr)
})
.into()
}
+86
View File
@@ -0,0 +1,86 @@
use defmt_parser::{Level, ParserMode};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error2::abort;
use quote::quote;
use syn::{parse_macro_input, parse_quote};
use crate::construct;
use self::env_filter::EnvFilter;
pub(crate) use self::{args::Args, codegen::Codegen};
mod args;
mod codegen;
mod env_filter;
pub(crate) fn expand(level: Level, args: TokenStream) -> TokenStream {
expand_parsed(level, parse_macro_input!(args as Args)).into()
}
pub(crate) fn expand_parsed(level: Level, args: Args) -> TokenStream2 {
let format_string = args.format_string.value();
let fragments = match defmt_parser::parse(&format_string, ParserMode::Strict) {
Ok(args) => args,
Err(e) => abort!(args.format_string, "{}", e),
};
let formatting_exprs = args
.formatting_args
.map(|punctuated| punctuated.into_iter().collect::<Vec<_>>())
.unwrap_or_default();
let Codegen { patterns, exprs } = Codegen::new(
&fragments,
formatting_exprs.len(),
args.format_string.span(),
);
let header = construct::interned_string(
&format_string,
level.as_str(),
true,
Some(level.as_str()),
&parse_quote!(defmt),
);
let env_filter = EnvFilter::from_env_var();
if let Some(filter_check) = env_filter.path_check(level) {
let content = if exprs.is_empty() {
quote!(
defmt::export::acquire_header_and_release(&#header);
)
} else {
quote!(
// safety: will be released a few lines further down
unsafe { defmt::export::acquire_and_header(&#header); };
#(#exprs;)*
// safety: acquire() was called a few lines above
unsafe { defmt::export::release() }
)
};
quote!(
{
option_env!("DEFMT_LOG");
match (#(&(#formatting_exprs)),*) {
(#(#patterns),*) => {
if #filter_check {
#content
}
}
}
}
)
} else {
// if logging is disabled match args, so they are not considered "unused"
quote!(
{
option_env!("DEFMT_LOG");
match (#(&(#formatting_exprs)),*) {
_ => {}
}
}
)
}
}
+24
View File
@@ -0,0 +1,24 @@
use syn::{
parse::{self, Parse, ParseStream},
punctuated::Punctuated,
Expr, LitStr, Token,
};
pub(crate) struct Args {
pub(crate) format_string: LitStr,
pub(crate) formatting_args: Option<Punctuated<Expr, Token![,]>>,
}
impl Parse for Args {
fn parse(input: ParseStream) -> parse::Result<Self> {
Ok(Self {
format_string: input.parse()?,
formatting_args: if input.is_empty() {
None
} else {
let _comma: Token![,] = input.parse()?;
Some(Punctuated::parse_terminated(input)?)
},
})
}
}
+140
View File
@@ -0,0 +1,140 @@
use defmt_parser::{Fragment, Parameter, Type};
use proc_macro2::{Ident as Ident2, Span as Span2, TokenStream as TokenStream2};
use proc_macro_error2::abort;
use quote::{format_ident, quote};
pub(crate) struct Codegen {
pub(crate) exprs: Vec<TokenStream2>,
pub(crate) patterns: Vec<Ident2>,
}
impl Codegen {
pub(crate) fn new(fragments: &[Fragment<'_>], given_arg_count: usize, span: Span2) -> Self {
let params = fragments
.iter()
.filter_map(|frag| match frag {
Fragment::Parameter(param) => Some(param.clone()),
Fragment::Literal(_) => None,
})
.collect::<Vec<_>>();
let expected_arg_count = params
.iter()
.map(|param| param.index + 1)
.max()
.unwrap_or(0);
if given_arg_count != expected_arg_count {
let mut only = "";
if given_arg_count < expected_arg_count {
only = "only ";
}
abort!(
span,
"format string requires {} arguments but {}{} were provided",
expected_arg_count,
only,
given_arg_count
)
}
let mut exprs = vec![];
let mut patterns = vec![];
for arg_index in 0..expected_arg_count {
let arg_ident = format_ident!("arg{}", arg_index);
let matching_param = params
.iter()
.find(|param| param.index == arg_index)
.unwrap();
let expr = encode_arg(&matching_param.ty, &params, arg_index, &arg_ident);
exprs.push(expr);
patterns.push(arg_ident);
}
Codegen { exprs, patterns }
}
}
fn encode_arg(ty: &Type, params: &[Parameter], arg_index: usize, arg: &Ident2) -> TokenStream2 {
match ty {
Type::I8 => quote!(defmt::export::i8(#arg)),
Type::I16 => quote!(defmt::export::i16(#arg)),
Type::I32 => quote!(defmt::export::i32(#arg)),
Type::I64 => quote!(defmt::export::i64(#arg)),
Type::I128 => quote!(defmt::export::i128(#arg)),
Type::Isize => quote!(defmt::export::isize(#arg)),
Type::U8 => quote!(defmt::export::u8(#arg)),
Type::U16 => quote!(defmt::export::u16(#arg)),
Type::U32 => quote!(defmt::export::u32(#arg)),
Type::U64 => quote!(defmt::export::u64(#arg)),
Type::U128 => quote!(defmt::export::u128(#arg)),
Type::Usize => quote!(defmt::export::usize(#arg)),
Type::F32 => quote!(defmt::export::f32(#arg)),
Type::F64 => quote!(defmt::export::f64(#arg)),
Type::Bool => quote!(defmt::export::bool(#arg)),
Type::Str => quote!(defmt::export::str(#arg)),
Type::IStr => quote!(defmt::export::istr(#arg)),
Type::Char => quote!(defmt::export::char(#arg)),
Type::Format => quote!(defmt::export::fmt(#arg)),
Type::FormatSlice => quote!(defmt::export::fmt_slice(#arg)),
Type::FormatArray(len) => quote!(defmt::export::fmt_array({
let tmp: &[_; #len] = #arg;
tmp
})),
Type::Debug => quote!(defmt::export::debug(#arg)),
Type::Display => quote!(defmt::export::display(#arg)),
Type::FormatSequence => unreachable!(),
Type::U8Slice => quote!(defmt::export::slice(#arg)),
// We cast to the expected array type (which should be a no-op cast) to provoke
// a type mismatch error on mismatched lengths:
// ``Symbol’s value as variable is void: //
Type::U8Array(len) => quote!(defmt::export::u8_array({
let tmp: &[u8; #len] = #arg;
tmp
})),
Type::BitField(_) => {
let all_bitfields = params.iter().filter(|param| param.index == arg_index);
let (smallest_bit_index, largest_bit_index) =
defmt_parser::get_max_bitfield_range(all_bitfields).unwrap();
// indices of the lowest and the highest octet which contains bitfield-relevant data
let lowest_byte = smallest_bit_index / 8;
let highest_byte = (largest_bit_index - 1) / 8;
let truncated_sz = highest_byte - lowest_byte + 1; // in bytes
// shift away unneeded lower octet
// TODO: create helper for shifting because readability
match truncated_sz {
1 => {
quote!(defmt::export::u8(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))
}
2 => {
quote!(defmt::export::u16(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))
}
3..=4 => {
quote!(defmt::export::u32(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))
}
5..=8 => {
quote!(defmt::export::u64(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))
}
9..=16 => {
quote!(defmt::export::u128(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))
}
_ => unreachable!(),
}
}
}
}
+367
View File
@@ -0,0 +1,367 @@
use std::{
collections::{BTreeMap, BTreeSet},
env,
};
use defmt_parser::Level;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error2::abort_call_site;
use quote::quote;
use self::parse::{Entry, LogLevelOrOff, ModulePath};
mod parse;
#[derive(Debug)]
pub(crate) struct EnvFilter {
// to keep the module paths sorted by length we use a btreemap
entries: BTreeMap<ModulePath, LogLevelOrOff>,
}
impl EnvFilter {
pub(crate) fn from_env_var() -> Self {
let defmt_log = env::var("DEFMT_LOG").ok();
let cargo_crate_name = env::var("CARGO_CRATE_NAME")
.unwrap_or_else(|_| abort_call_site!("`CARGO_CRATE_NAME` env var is not set"));
Self::new(defmt_log.as_deref(), &cargo_crate_name)
}
fn new(defmt_log: Option<&str>, cargo_crate_name: &str) -> Self {
// match `env_logger` behavior
const LEVEL_WHEN_LEVEL_IS_NOT_SPECIFIED: LogLevelOrOff = Some(Level::Trace);
const LEVEL_WHEN_NOTHING_IS_SPECIFIED: LogLevelOrOff = Some(Level::Error);
let caller_crate = cargo_crate_name;
let mut entries = BTreeMap::new();
let mut fallback_log_level = None;
if let Some(input) = defmt_log {
for entry in parse::defmt_log(input) {
let (modpath, level) = match entry {
Entry::LogLevel(log_level) => {
if fallback_log_level.is_none() {
fallback_log_level = Some(log_level);
}
continue;
}
Entry::ModulePath(module) => (module, LEVEL_WHEN_LEVEL_IS_NOT_SPECIFIED),
Entry::ModulePathLogLevel {
module_path,
log_level,
} => (module_path, log_level),
};
if modpath.crate_name() == caller_crate && !entries.contains_key(&modpath) {
entries.insert(modpath, level);
}
}
}
let modpath = ModulePath::from_crate_name(caller_crate);
entries
.entry(modpath)
.or_insert_with(|| fallback_log_level.unwrap_or(LEVEL_WHEN_NOTHING_IS_SPECIFIED));
EnvFilter { entries }
}
/// Builds a compile-time check that returns `true` when `module_path!` can emit logs at the
/// requested log `level`
///
/// Returns `None` if the caller crate (at any module path) will never emit logs at requested log `level`
pub(crate) fn path_check(&self, level: Level) -> Option<TokenStream2> {
enum Criteria {
Accept,
Reject,
}
let modules_to_accept = self.modules_on_for(level);
if modules_to_accept.is_empty() {
return None;
}
let modules_to_reject = self.always_off_modules();
let module_to_criteria: BTreeMap<_, _> = modules_to_accept
.iter()
.map(|&path| (path, Criteria::Accept))
.chain(
modules_to_reject
.iter()
.map(|&path| (path, Criteria::Reject)),
)
.collect();
// iterate in reverse because we want to early accept innermost modules
// the iteration will go `krate::module::inner`, then `krate::module` then `krate`
let checks = module_to_criteria
.iter()
.rev()
.map(|(&module_path, criteria)| {
let check = codegen_is_inside_of_check(&module_path.to_string());
let retval = match criteria {
Criteria::Accept => quote!(true),
Criteria::Reject => quote!(false),
};
quote!(if #check {
return #retval;
})
})
.collect::<Vec<_>>();
Some(quote!({
const CHECK: bool = {
const fn check() -> bool {
let module_path = module_path!().as_bytes();
#(#checks)*
false
}
check()
};
CHECK
}))
}
/// Returns the set of modules that can emit logs at requested `level`
fn modules_on_for(&self, level: Level) -> BTreeSet<&ModulePath> {
self.entries
.iter()
.rev()
.filter_map(|(module_path, min_level)| {
// `min_level == None` means "off" so exclude the module path in that case
min_level.and_then(|min_level| {
if level >= min_level {
Some(module_path)
} else {
None
}
})
})
.collect()
}
/// Returns the set of modules that must NOT emit logs (= that are set to `off`)
fn always_off_modules(&self) -> BTreeSet<&ModulePath> {
self.entries
.iter()
.rev()
.filter_map(|(module_path, level_or_off)| {
if level_or_off.is_none() {
// `off` pseudo-level
Some(module_path)
} else {
None
}
})
.collect()
}
}
// NOTE this also returns `true` when `module_path == parent_module_path`
// what we want to check is if the function that calls the proc-macro is inside `parent_module_path`
// `module_path!` returns the path to the module the function is in, not the path to the function
// itself
fn codegen_is_inside_of_check(parent_module_path: &str) -> TokenStream2 {
let parent = parent_module_path.as_bytes();
let parent_len = parent.len();
let byte_checks = parent
.iter()
.enumerate()
.map(|(index, byte)| quote!(module_path[#index] == #byte))
.collect::<Vec<_>>();
quote!(
// start of const-context `[u8]::starts_with(needle)`
if #parent_len > module_path.len() {
false
} else {
#(#byte_checks &&)*
// end of const-context `[u8]::starts_with`
// check that what follows the `module_path` is the end of a path segment
if #parent_len == module_path.len() {
// end of the entire module path
true
} else {
// end of module path _segment_
//
// `module_path` comes from `module_path!`; we assume it's well-formed so we
// don't check *everything* that comes after `needle`; just the first
// character of what should be the path separator ("::")
module_path[#parent_len] == b':'
}
})
}
#[cfg(test)]
mod tests {
use maplit::btreeset;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn when_duplicates_entries_in_defmt_log_use_last_entry() {
let env_filter = EnvFilter::new(Some("krate=info,krate=debug"), "krate");
let expected = [ModulePath::parse("krate")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Debug)
);
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Trace));
}
#[test]
fn when_empty_defmt_log_use_error() {
let env_filter = EnvFilter::new(None, "krate");
let expected = [ModulePath::parse("krate")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Error)
);
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Warn));
}
#[test]
fn when_no_level_in_defmt_log_use_trace() {
let env_filter = EnvFilter::new(Some("krate"), "krate");
let expected = [ModulePath::parse("krate")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Trace)
);
}
#[test]
fn when_level_in_defmt_log_use_it() {
let env_filter = EnvFilter::new(Some("krate=info"), "krate");
let expected = [ModulePath::parse("krate")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Info)
);
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Debug));
}
#[test]
fn when_only_level_is_specified_in_defmt_log_it_applies_to_all_crates() {
let env_filter = EnvFilter::new(Some("info"), "krate");
let expected = [ModulePath::parse("krate")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Info)
);
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Debug));
}
#[test]
fn moduleless_level_has_lower_precedence() {
let env_filter = EnvFilter::new(Some("krate=info,warn"), "krate");
let expected = [ModulePath::parse("krate")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Info)
);
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Debug));
}
#[test]
fn moduleless_level_behaves_like_a_krate_level_pair() {
let env_filter = EnvFilter::new(Some("krate::module=info,warn"), "krate");
let expected = [
ModulePath::parse("krate"),
ModulePath::parse("krate::module"),
];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Warn)
);
let expected = [ModulePath::parse("krate::module")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Info)
);
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Debug));
}
#[test]
fn module_paths_different_levels() {
let env_filter = EnvFilter::new(Some("krate=info,krate::module=debug"), "krate");
let expected = [
ModulePath::parse("krate"),
ModulePath::parse("krate::module"),
];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Info)
);
let expected = [ModulePath::parse("krate::module")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Debug)
);
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Trace));
}
#[test]
fn blanket_off() {
let env_filter = EnvFilter::new(Some("off"), "krate");
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Error));
let expected = [ModulePath::parse("krate")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.always_off_modules()
);
}
#[test]
fn blanket_off_plus_override() {
let env_filter = EnvFilter::new(Some("krate::module=error,off"), "krate");
let expected = [ModulePath::parse("krate::module")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Error)
);
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Warn));
let expected = [ModulePath::parse("krate")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.always_off_modules()
);
}
#[test]
fn does_not_match_partial_crate_name() {
let env_filter = EnvFilter::new(Some("fooo=warn"), "foo");
let expected = [ModulePath::parse("foo")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Error)
);
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Warn));
}
// doesn't affect runtime performance but it makes the expanded code smaller
#[ignore = "TODO(P-low/optimization): impl & more test cases"]
#[test]
fn when_module_paths_with_same_level_remove_inner_ones() {
let env_filter = EnvFilter::new(Some("krate=info,krate::module=info"), "krate");
let expected = [ModulePath::parse("krate")];
assert_eq!(
expected.iter().collect::<BTreeSet<_>>(),
env_filter.modules_on_for(Level::Info)
);
assert_eq!(btreeset![], env_filter.modules_on_for(Level::Debug));
}
}
@@ -0,0 +1,184 @@
use defmt_parser::Level;
#[cfg(not(test))]
use proc_macro_error2::abort_call_site as panic;
use std::fmt;
use syn::Ident;
// None = "off" pseudo-level
pub(crate) type LogLevelOrOff = Option<Level>;
// NOTE this is simpler than `syn::Path`; we do not want to accept e.g. `Vec::<Ty>::new`
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct ModulePath {
segments: Vec<String>,
}
/// Parses the contents of the `DEFMT_LOG` env var
pub(crate) fn defmt_log(input: &str) -> impl Iterator<Item = Entry> + '_ {
input
.rsplit(',')
.filter(|entry| !entry.is_empty())
.map(|entry| {
if let Some((path, log_level)) = entry.rsplit_once('=') {
let module_path = ModulePath::parse(path);
let log_level = parse_log_level(log_level).unwrap_or_else(|_| {
panic!(
"unknown log level `{}` in DEFMT_LOG env var. \
expected one of: off, error, info, warn, debug, trace",
log_level
)
});
Entry::ModulePathLogLevel {
module_path,
log_level,
}
} else if let Ok(log_level) = parse_log_level(entry) {
Entry::LogLevel(log_level)
} else {
Entry::ModulePath(ModulePath::parse(entry))
}
})
}
#[derive(Debug, PartialEq)]
pub(crate) enum Entry {
LogLevel(LogLevelOrOff),
ModulePath(ModulePath),
ModulePathLogLevel {
module_path: ModulePath,
log_level: LogLevelOrOff,
},
}
impl ModulePath {
pub(crate) fn from_crate_name(input: &str) -> Self {
if input.is_empty() && input.contains("::") {
panic!(
"DEFMT_LOG env var: crate name cannot be an empty string or contain path separators"
)
}
Self::parse(input)
}
pub(super) fn parse(input: &str) -> Self {
if input.is_empty() {
panic!("DEFMT_LOG env var: module path cannot be an empty string")
}
input.split("::").for_each(validate_identifier);
Self {
segments: input
.split("::")
.map(|segment| segment.to_string())
.collect(),
}
}
pub(super) fn crate_name(&self) -> &str {
&self.segments[0]
}
}
impl fmt::Display for ModulePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.segments.join("::"))
}
}
fn parse_log_level(input: &str) -> Result<LogLevelOrOff, ()> {
Ok(Some(match input {
"debug" => Level::Debug,
"error" => Level::Error,
"info" => Level::Info,
"off" => return Ok(None),
"trace" => Level::Trace,
"warn" => Level::Warn,
_ => return Err(()),
}))
}
fn validate_identifier(input: &str) {
syn::parse_str::<Ident>(input)
.unwrap_or_else(|_| panic!("`{input}` is not a valid identifier"));
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
#[test]
fn parses_from_the_right() {
let entries = defmt_log("krate=info,krate,info").collect::<Vec<_>>();
assert_eq!(
[
Entry::LogLevel(Some(Level::Info)),
Entry::ModulePath(ModulePath {
segments: vec!["krate".to_string()]
}),
Entry::ModulePathLogLevel {
module_path: ModulePath {
segments: vec!["krate".to_string()]
},
log_level: Some(Level::Info)
},
],
entries.as_slice()
);
}
#[test]
fn after_sorting_innermost_modules_appear_last() {
let mut paths = [
ModulePath::parse("krate::module::inner"),
ModulePath::parse("krate"),
ModulePath::parse("krate::module"),
];
paths.sort();
let expected = [
ModulePath::parse("krate"),
ModulePath::parse("krate::module"),
ModulePath::parse("krate::module::inner"),
];
assert_eq!(expected, paths);
}
#[test]
fn accepts_raw_identifier() {
ModulePath::parse("krate::r#mod");
}
#[rstest]
#[case::has_module("krate::module")]
#[case::no_module("krate")]
fn modpath_crate_name(#[case] input: &str) {
let modpath = ModulePath::parse(input);
assert_eq!("krate", modpath.crate_name());
}
#[rstest]
#[case::crate_name_is_invalid("some-crate::module")]
#[case::module_name_is_invalid("krate::some-module")]
#[case::with_level("krate::some-module=info")]
#[should_panic = "not a valid identifier"]
fn rejects_invalid_identifier(#[case] input: &str) {
defmt_log(input).next();
}
#[test]
#[should_panic = "unknown log level"]
fn rejects_unknown_log_level() {
defmt_log("krate=module").next();
}
#[test]
#[should_panic = "module path cannot be an empty string"]
fn rejects_empty_module_path() {
defmt_log("=info").next();
}
}
+42
View File
@@ -0,0 +1,42 @@
use std::borrow::Cow;
use defmt_parser::Level;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
use crate::{construct, function_like::log};
pub(crate) fn expand(
args: TokenStream,
zero_args_format_string: &str,
transform_format_string: impl FnOnce(&str) -> String,
) -> TokenStream {
let (format_string, formatting_args) = if args.is_empty() {
// panic!() -> error!("panicked at 'explicit panic'")
(Cow::from(zero_args_format_string), None)
} else {
// panic!("a", b, c) -> error!("panicked at 'a'", b, c)
let log_args = parse_macro_input!(args as log::Args);
let format_string = transform_format_string(&log_args.format_string.value());
(Cow::from(format_string), log_args.formatting_args)
};
let format_string = construct::string_literal(&format_string);
let log_stmt = log::expand_parsed(
Level::Error,
log::Args {
format_string,
formatting_args,
},
);
quote!(
{
#log_stmt;
defmt::export::panic()
}
)
.into()
}
+59
View File
@@ -0,0 +1,59 @@
use defmt_parser::ParserMode;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error2::abort;
use quote::quote;
use syn::{parse_macro_input, parse_quote};
use crate::construct;
use crate::function_like::log::{Args, Codegen};
pub(crate) fn expand(args: TokenStream) -> TokenStream {
expand_parsed(parse_macro_input!(args as Args)).into()
}
pub(crate) fn expand_parsed(args: Args) -> TokenStream2 {
let format_string = args.format_string.value();
let fragments = match defmt_parser::parse(&format_string, ParserMode::Strict) {
Ok(args) => args,
Err(e @ defmt_parser::Error::UnknownDisplayHint(_)) => abort!(
args.format_string, "{}", e;
help = "`defmt` uses a slightly different syntax than regular formatting in Rust. See https://defmt.ferrous-systems.com/macros.html for more details.";
),
Err(e) => abort!(args.format_string, "{}", e), // No extra help
};
let formatting_exprs = args
.formatting_args
.map(|punctuated| punctuated.into_iter().collect::<Vec<_>>())
.unwrap_or_default();
let Codegen { patterns, exprs } = Codegen::new(
&fragments,
formatting_exprs.len(),
args.format_string.span(),
);
let header =
construct::interned_string(&format_string, "println", true, None, &parse_quote!(defmt));
let content = if exprs.is_empty() {
quote!(
defmt::export::acquire_header_and_release(&#header);
)
} else {
quote!(
// safety: will be released a few lines further down
unsafe { defmt::export::acquire_and_header(&#header); };
#(#exprs;)*
// safety: acquire() was called a few lines above
unsafe { defmt::export::release() }
)
};
quote!({
match (#(&(#formatting_exprs)),*) {
(#(#patterns),*) => {
#content
}
}
})
}
+49
View File
@@ -0,0 +1,49 @@
use defmt_parser::ParserMode;
use proc_macro::TokenStream;
use proc_macro_error2::abort;
use quote::quote;
use syn::{parse_macro_input, parse_quote};
use crate::{construct, function_like::log};
use self::args::Args;
mod args;
pub(crate) fn expand(args: TokenStream) -> TokenStream {
let Args {
formatter,
log_args,
..
} = parse_macro_input!(args as Args);
let format_string = log_args.format_string.value();
let fragments = match defmt_parser::parse(&format_string, ParserMode::Strict) {
Ok(args) => args,
Err(e) => abort!(log_args.format_string, "{}", e),
};
let formatting_exprs: Vec<_> = log_args
.formatting_args
.map(|punctuated| punctuated.into_iter().collect())
.unwrap_or_default();
let log::Codegen { patterns, exprs } = log::Codegen::new(
&fragments,
formatting_exprs.len(),
log_args.format_string.span(),
);
let format_tag =
construct::interned_string(&format_string, "write", false, None, &parse_quote!(defmt));
quote!({
let _typecheck_formatter: defmt::Formatter<'_> = #formatter;
match (#(&(#formatting_exprs)),*) {
(#(#patterns),*) => {
defmt::export::istr(&#format_tag);
#(#exprs;)*
}
}
})
.into()
}
+22
View File
@@ -0,0 +1,22 @@
use syn::{
parse::{self, Parse, ParseStream},
Expr, Token,
};
use crate::function_like::log;
pub(crate) struct Args {
pub(crate) formatter: Expr,
_comma: Token![,],
pub(crate) log_args: log::Args,
}
impl Parse for Args {
fn parse(input: ParseStream) -> parse::Result<Self> {
Ok(Self {
formatter: input.parse()?,
_comma: input.parse()?,
log_args: input.parse()?,
})
}
}
+4
View File
@@ -0,0 +1,4 @@
//! Procedural macros that expand to items
pub(crate) mod bitflags;
pub(crate) mod timestamp;
+99
View File
@@ -0,0 +1,99 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, parse_quote};
use crate::{cargo, construct};
use self::input::Input;
mod input;
pub(crate) fn expand(input: TokenStream) -> TokenStream {
let bitflags_input = TokenStream2::from(input.clone());
let input = parse_macro_input!(input as Input);
// Encode package and disambiguator to provide the decoder with all info it needs (even if
// technically redundant, since it's also stored in the symbol we create).
let format_string = format!(
"{{={}:__internal_bitflags_{}@{}@{}@{}}}",
input.ty().to_token_stream(),
input.ident(),
cargo::package_name(),
construct::crate_local_disambiguator(),
cargo::crate_name(),
);
let format_tag = construct::interned_string(
&format_string,
"bitflags",
false,
None,
&parse_quote!(defmt),
);
let ident = input.ident();
let ty = input.ty();
let flag_statics = codegen_flag_statics(&input);
quote!(
const _: () = {
fn assert<T: defmt::export::UnsignedInt>() {}
assert::<#ty>;
#(#flag_statics)*
};
defmt::export::bitflags! {
#bitflags_input
}
impl defmt::Format for #ident {
fn format(&self, f: defmt::Formatter) {
defmt::unreachable!()
}
fn _format_tag() -> defmt::Str {
#format_tag
}
fn _format_data(&self) {
// There's a method available for every supported bitflags type.
defmt::export::#ty(&self.bits());
}
}
)
.into()
}
fn codegen_flag_statics(input: &Input) -> Vec<TokenStream2> {
input
.flags()
.enumerate()
.map(|(i, flag)| {
let cfg_attrs = flag.cfg_attrs();
let var_name = flag.ident();
let struct_name = input.ident();
let repr_ty = input.ty();
let sym_name = construct::mangled_symbol_name(
"bitflags_value",
&format!("{}::{i}::{}", input.ident(), flag.ident()),
);
quote! {
#(#cfg_attrs)*
#[cfg_attr(target_os = "macos", link_section = ".defmt,end")]
#[cfg_attr(not(target_os = "macos"), link_section = ".defmt.end")]
#[export_name = #sym_name]
static #var_name: u128 = {
// NB: It might be tempting to just do `#value as u128` here, but that
// causes a value such as `1 << 127` to be evaluated as an `i32`, which
// overflows. So we instead coerce (but don't cast) it to the bitflags' raw
// type, and then cast that to u128.
let coerced_value: #repr_ty = #struct_name::#var_name.bits;
coerced_value as u128
};
}
})
.collect::<Vec<_>>()
}
+93
View File
@@ -0,0 +1,93 @@
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
token, Attribute, Expr, Ident, Token, Type, Visibility,
};
#[allow(dead_code)]
pub(super) struct Input {
struct_attrs: Vec<Attribute>,
vis: Visibility,
struct_token: Token![struct],
ident: Ident,
colon_token: Token![:],
ty: Type,
brace_token: token::Brace,
flags: Punctuated<Flag, Token![;]>,
}
impl Parse for Input {
fn parse(input: ParseStream) -> syn::Result<Self> {
let flags;
Ok(Self {
struct_attrs: Attribute::parse_outer(input)?,
vis: input.parse()?,
struct_token: input.parse()?,
ident: input.parse()?,
colon_token: input.parse()?,
ty: input.parse()?,
brace_token: syn::braced!(flags in input),
flags: Punctuated::parse_terminated(&flags)?,
})
}
}
impl Input {
pub(super) fn flags(&self) -> impl Iterator<Item = &Flag> {
self.flags.iter()
}
pub(super) fn ident(&self) -> &Ident {
&self.ident
}
pub(super) fn ty(&self) -> &Type {
&self.ty
}
}
#[allow(dead_code)]
pub(super) struct Flag {
cfg_attrs: Vec<Attribute>,
const_attrs: Vec<Attribute>,
const_token: Token![const],
ident: Ident,
eq_token: Token![=],
value: Expr,
}
impl Parse for Flag {
fn parse(input: ParseStream) -> syn::Result<Self> {
let const_attrs = Attribute::parse_outer(input)?;
Ok(Self {
cfg_attrs: extract_cfgs(&const_attrs),
const_attrs,
const_token: input.parse()?,
ident: input.parse()?,
eq_token: input.parse()?,
value: input.parse()?,
})
}
}
impl Flag {
pub(super) fn cfg_attrs(&self) -> &[Attribute] {
&self.cfg_attrs
}
pub(super) fn ident(&self) -> &Ident {
&self.ident
}
}
fn extract_cfgs(attrs: &[Attribute]) -> Vec<Attribute> {
let mut cfgs = vec![];
for attr in attrs {
if attr.path().is_ident("cfg") {
cfgs.push(attr.clone());
}
}
cfgs
}
+59
View File
@@ -0,0 +1,59 @@
use defmt_parser::ParserMode;
use proc_macro::TokenStream;
use proc_macro_error2::abort;
use quote::format_ident;
use quote::quote;
use syn::parse_macro_input;
use crate::{construct, function_like::log};
pub(crate) fn expand(args: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as log::Args);
let format_string = args.format_string.value();
let fragments = match defmt_parser::parse(&format_string, ParserMode::Strict) {
Ok(args) => args,
Err(e) => abort!(args.format_string, "{}", e),
};
let formatting_exprs: Vec<_> = args
.formatting_args
.map(|punctuated| punctuated.into_iter().collect())
.unwrap_or_default();
let log::Codegen { patterns, exprs } = log::Codegen::new(
&fragments,
formatting_exprs.len(),
args.format_string.span(),
);
let var_name = format_ident!("S");
let var_item = construct::static_variable(&var_name, &format_string, "timestamp", None);
quote!(
const _: () = {
#[export_name = "_defmt_timestamp"]
#[inline(never)]
fn defmt_timestamp(fmt: defmt::Formatter<'_>) {
match (#(&(#formatting_exprs)),*) {
(#(#patterns),*) => {
// NOTE: No format string index, and no finalize call.
#(#exprs;)*
}
}
}
#var_item;
// Unique symbol name to prevent multiple `timestamp!` invocations in the crate graph.
// Uses `#var_name` to ensure it is not discarded by the linker.
// This symbol itself is retained via a `EXTERN` directive in the linker script.
#[no_mangle]
#[cfg_attr(target_os = "macos", link_section = ".defmt,end.timestamp")]
#[cfg_attr(not(target_os = "macos"), link_section = ".defmt.end.timestamp")]
static __DEFMT_MARKER_TIMESTAMP_WAS_DEFINED: &u8 = &#var_name;
};
)
.into()
}
+209
View File
@@ -0,0 +1,209 @@
//! INTERNAL; DO NOT USE. Please use the `defmt` crate to access the functionality implemented here
#![doc(html_logo_url = "https://knurling.ferrous-systems.com/knurling_logo_light_text.svg")]
use defmt_parser::Level;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error2::proc_macro_error;
use quote::quote;
mod attributes;
mod cargo;
mod construct;
mod consts;
mod derives;
mod function_like;
mod items;
// NOTE some proc-macro functions have an `_` (underscore) suffix. This is intentional.
// If left unsuffixed the procedural macros would shadow macros defined in `std` (like `assert`)
// within the context of this entire crate, which leads to lots of confusion.
/* # Attributes */
#[proc_macro_attribute]
#[proc_macro_error]
pub fn global_logger(args: TokenStream, item: TokenStream) -> TokenStream {
attributes::global_logger::expand(args, item)
}
#[proc_macro_attribute]
#[proc_macro_error]
pub fn panic_handler(args: TokenStream, item: TokenStream) -> TokenStream {
attributes::panic_handler::expand(args, item)
}
/* # Derives */
#[proc_macro_derive(Format, attributes(defmt))]
#[proc_macro_error]
pub fn format(input: TokenStream) -> TokenStream {
derives::format::expand(input)
}
/* # Function-like */
#[proc_macro]
#[proc_macro_error]
pub fn assert_(args: TokenStream) -> TokenStream {
function_like::assert_like::assert::expand(args)
}
#[proc_macro]
#[proc_macro_error]
pub fn assert_eq_(args: TokenStream) -> TokenStream {
function_like::assert_binop::eq(args)
}
#[proc_macro]
#[proc_macro_error]
pub fn assert_ne_(args: TokenStream) -> TokenStream {
function_like::assert_binop::ne(args)
}
/* ## `debug_` variants */
// NOTE these `debug_*` macros can be written using `macro_rules!` (that'd be simpler) but that
// results in an incorrect source code location being reported: the location of the `macro_rules!`
// statement is reported. Using a proc-macro results in the call site being reported, which is what
// we want
#[proc_macro]
#[proc_macro_error]
pub fn debug_assert_(input: TokenStream) -> TokenStream {
let assert = TokenStream2::from(assert_(input));
quote!(if cfg!(debug_assertions) {
#assert
})
.into()
}
#[proc_macro]
#[proc_macro_error]
pub fn debug_assert_eq_(input: TokenStream) -> TokenStream {
let assert = TokenStream2::from(assert_eq_(input));
quote!(if cfg!(debug_assertions) {
#assert
})
.into()
}
#[proc_macro]
#[proc_macro_error]
pub fn debug_assert_ne_(input: TokenStream) -> TokenStream {
let assert = TokenStream2::from(assert_ne_(input));
quote!(if cfg!(debug_assertions) {
#assert
})
.into()
}
/* ## end of `debug_` variants */
#[proc_macro]
#[proc_macro_error]
pub fn dbg(args: TokenStream) -> TokenStream {
function_like::dbg::expand(args)
}
#[proc_macro]
#[proc_macro_error]
pub fn intern(args: TokenStream) -> TokenStream {
function_like::intern::expand(args)
}
#[proc_macro]
#[proc_macro_error]
pub fn internp(args: TokenStream) -> TokenStream {
function_like::internp::expand(args)
}
#[proc_macro]
#[proc_macro_error]
pub fn println(args: TokenStream) -> TokenStream {
function_like::println::expand(args)
}
/* ## Logging macros */
#[proc_macro]
#[proc_macro_error]
pub fn trace(args: TokenStream) -> TokenStream {
function_like::log::expand(Level::Trace, args)
}
#[proc_macro]
#[proc_macro_error]
pub fn debug(args: TokenStream) -> TokenStream {
function_like::log::expand(Level::Debug, args)
}
#[proc_macro]
#[proc_macro_error]
pub fn info(args: TokenStream) -> TokenStream {
function_like::log::expand(Level::Info, args)
}
#[proc_macro]
#[proc_macro_error]
pub fn warn(args: TokenStream) -> TokenStream {
function_like::log::expand(Level::Warn, args)
}
#[proc_macro]
#[proc_macro_error]
pub fn error(args: TokenStream) -> TokenStream {
function_like::log::expand(Level::Error, args)
}
/* ## end of logging macros */
#[proc_macro]
#[proc_macro_error]
pub fn panic_(args: TokenStream) -> TokenStream {
function_like::panic_like::expand(args, "panicked at 'explicit panic'", |format_string| {
format!("panicked at '{format_string}'")
})
}
#[proc_macro]
#[proc_macro_error]
pub fn todo_(args: TokenStream) -> TokenStream {
function_like::panic_like::expand(args, "panicked at 'not yet implemented'", |format_string| {
format!("panicked at 'not yet implemented: {format_string}'")
})
}
#[proc_macro]
#[proc_macro_error]
pub fn unreachable_(args: TokenStream) -> TokenStream {
function_like::panic_like::expand(
args,
"panicked at 'internal error: entered unreachable code'",
|format_string| {
format!(
"panicked at 'internal error: entered unreachable code: {}'",
format_string
)
},
)
}
#[proc_macro]
#[proc_macro_error]
pub fn unwrap(args: TokenStream) -> TokenStream {
function_like::assert_like::unwrap::expand(args)
}
#[proc_macro]
#[proc_macro_error]
pub fn write(args: TokenStream) -> TokenStream {
function_like::write::expand(args)
}
/* # Items */
#[proc_macro]
#[proc_macro_error]
pub fn bitflags(ts: TokenStream) -> TokenStream {
items::bitflags::expand(ts)
}
#[proc_macro]
#[proc_macro_error]
pub fn timestamp(args: TokenStream) -> TokenStream {
items::timestamp::expand(args)
}