From 93dcfff911be1811dd40fb8e48c302372bbdf009 Mon Sep 17 00:00:00 2001 From: agrif Date: Tue, 18 Jun 2024 04:22:14 -0400 Subject: [PATCH] Add optional defmt implementation (#42) * add optional defmt::Format impl generation * add tests and info to README.md about defmt * remove redundant #[cfg(test)] in test file * consolidate debug/defmt implementation code into new function --- Cargo.lock | 101 ++++++++++++++++++++++++++++++++- Cargo.toml | 1 + README.md | 14 +++++ src/lib.rs | 152 +++++++++++++++++++++++++++++++++++++++++--------- tests/test.rs | 100 +++++++++++++++++++++++++++++++++ 5 files changed, 341 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 680330b..fcb63ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,10 +6,49 @@ version = 3 name = "bitfield-struct" version = "0.7.0" dependencies = [ + "defmt", "endian-num", "proc-macro2", "quote", - "syn", + "syn 2.0.65", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "defmt" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99dd22262668b887121d4672af5a64b238f026099f1a2a1b322066c9ecfe9e0" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9f309eff1f79b3ebdf252954d90ae440599c26c2c553fe87a2d17195f2dcb" +dependencies = [ + "defmt-parser", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.65", +] + +[[package]] +name = "defmt-parser" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4a5fefe330e8d7f31b16a318f9ce81000d8e35e69b93eae154d16d2278f70f" +dependencies = [ + "thiserror", ] [[package]] @@ -18,6 +57,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad847bb2094f110bbdd6fa564894ca4556fd978958e93985420d680d3cb6d14" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.83" @@ -36,6 +99,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.65" @@ -47,8 +120,34 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/Cargo.toml b/Cargo.toml index 0e52f47..bfaf222 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,5 @@ syn = { version = "2.0", features = ["full"] } proc-macro2 = "1.0" [dev-dependencies] +defmt = "0.3" endian-num = { version = "0.1", features = ["linux-types"] } diff --git a/README.md b/README.md index f630ab0..6cbff8a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ As this library provides a procedural macro, it has no runtime dependencies and - Rust-analyzer friendly (carries over documentation to accessor functions) - Exports field offsets and sizes as constants (useful for const asserts) - Generation of `fmt::Debug` and `Default` +- Optional generation of `defmt::Format` ## Usage @@ -395,3 +396,16 @@ impl Default for CustomDebug { let val = CustomDebug::default(); println!("{val:?}") ``` + +## `defmt::Format` + +This macro can automatically implement a `defmt::Format` that mirrors the default `fmt::Debug` implementation by passing the extra `defmt` argument. This implementation requires the defmt crate to be available as `defmt`, and has the same rules and caveats as `#[derive(defmt::Format)]`. + +```rust +use bitfield_struct::bitfield; + +#[bitfield(u64, defmt = true)] +struct DefmtExample { + data: u64 +} +```` diff --git a/src/lib.rs b/src/lib.rs index c241142..0b7f9c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ fn s_err(span: proc_macro2::Span, msg: impl fmt::Display) -> syn::Error { /// - `from` to specify a conversion function from repr to the bitfield's integer type /// - `into` to specify a conversion function from the bitfield's integer type to repr /// - `debug` to disable the `Debug` trait generation +/// - `defmt` to enable the `defmt::Format` trait generation. /// - `default` to disable the `Default` trait generation /// - `order` to specify the bit order (Lsb, Msb) /// - `conversion` to disable the generation of into_bits and from_bits @@ -55,6 +56,7 @@ fn bitfield_inner(args: TokenStream, input: TokenStream) -> syn::Result syn::Result) -> core::fmt::Result { - f.debug_struct(stringify!(#name)) - #( #debug_fields )* - .finish() - } - } - } - } else { - quote!() - }; + let debug_impl = implement_debug(debug, defmt, &name, &members); let defaults = members.iter().map(Member::default); @@ -183,6 +172,111 @@ fn bitfield_inner(args: TokenStream, input: TokenStream) -> syn::Result TokenStream { + let debug_impl = if debug { + let fields = members.iter().flat_map(|m| { + let inner = m.inner.as_ref()?; + + if inner.from.is_empty() { + return None; + } + + let ident = &inner.ident; + Some(quote!(.field(stringify!(#ident), &self.#ident()))) + }); + + quote! { + impl core::fmt::Debug for #name { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct(stringify!(#name)) + #( #fields )* + .finish() + } + } + } + } else { + quote!() + }; + + let defmt_impl = if defmt { + // build a part of the format string for each field + let formats = members.iter().flat_map(|m| { + let inner = m.inner.as_ref()?; + + if inner.from.is_empty() { + return None; + } + + // default to using {:?} + let mut spec = "{:?}".to_owned(); + + // primitives supported by defmt + const PRIMITIVES: &[&str] = &[ + "bool", "usize", "isize", "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", + "i64", "i128", "f32", "f64", + ]; + + // get the type name so we can use more efficient defmt formats + // if it's a primitive + if let syn::Type::Path(syn::TypePath { path, .. }) = &inner.ty { + if let Some(ident) = path.get_ident() { + if PRIMITIVES.iter().any(|s| ident == s) { + // defmt supports this primitive, use special spec + spec = format!("{{={}}}", ident); + } + } + } + + let ident = &inner.ident; + Some(format!("{ident}: {spec}")) + }); + + // find the corresponding format argument for each field + let args = members.iter().flat_map(|m| { + let inner = m.inner.as_ref()?; + + if inner.from.is_empty() { + return None; + } + + let ident = &inner.ident; + Some(quote!(self.#ident())) + }); + + // build a string like "Foo { field_name: {:?}, ... }" + // four braces, two to escape *this* format, times two to escape + // the defmt::write! call below. + let format_string = format!( + "{} {{{{ {} }}}} ", + name, + formats.collect::>().join(", ") + ); + + // note: we use defmt paths here, not ::defmt, because many crates + // in the embedded space will rename defmt (e.g. to defmt_03) in + // order to support multiple incompatible defmt versions. + // + // defmt itself avoids ::defmt for this reason. For more info, see: + // https://github.com/knurling-rs/defmt/pull/835 + + quote! { + impl defmt::Format for #name { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, #format_string, #( #args, )*) + } + } + } + } else { + quote!() + }; + + quote!( + #debug_impl + + #defmt_impl + ) +} + /// Represents a member where accessor functions should be generated for. struct Member { offset: usize, @@ -306,16 +400,6 @@ impl Member { } } - fn debug(&self) -> TokenStream { - if let Some(inner) = &self.inner { - if !inner.from.is_empty() { - let ident = &inner.ident; - return quote!(.field(stringify!(#ident), &self.#ident())); - } - } - quote!() - } - fn default(&self) -> TokenStream { let default = &self.default; @@ -686,6 +770,7 @@ struct Params { from: Option, bits: usize, debug: bool, + defmt: bool, default: bool, order: Order, conversion: bool, @@ -705,6 +790,7 @@ impl Parse for Params { let mut from = None; let mut into = None; let mut debug = true; + let mut defmt = false; let mut default = true; let mut order = Order::Lsb; let mut conversion = true; @@ -726,6 +812,9 @@ impl Parse for Params { "debug" => { debug = syn::LitBool::parse(input)?.value; } + "defmt" => { + defmt = syn::LitBool::parse(input)?.value; + } "default" => { default = syn::LitBool::parse(input)?.value; } @@ -757,6 +846,7 @@ impl Parse for Params { into, bits, debug, + defmt, default, order, conversion, @@ -800,11 +890,21 @@ mod test { fn parse_args() { let args = quote!(u64); let params = syn::parse2::(args).unwrap(); - assert!(params.bits == u64::BITS as usize && params.debug == true); + assert_eq!(params.bits, u64::BITS as usize); + assert_eq!(params.debug, true); + assert_eq!(params.defmt, false); let args = quote!(u32, debug = false); let params = syn::parse2::(args).unwrap(); - assert!(params.bits == u32::BITS as usize && params.debug == false); + assert_eq!(params.bits, u32::BITS as usize); + assert_eq!(params.debug, false); + assert_eq!(params.defmt, false); + + let args = quote!(u32, defmt = true); + let params = syn::parse2::(args).unwrap(); + assert_eq!(params.bits, u32::BITS as usize); + assert_eq!(params.debug, true); + assert_eq!(params.defmt, true); let args = quote!(u32, order = Msb); let params = syn::parse2::(args).unwrap(); diff --git a/tests/test.rs b/tests/test.rs index 0e09fdf..ba96306 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -133,6 +133,106 @@ fn debug() { println!("{full:?}"); } +// a dummy defmt logger and timestamp implementation, for testing +mod defmt_logger { + #[defmt::global_logger] + struct Logger; + + unsafe impl defmt::Logger for Logger { + fn acquire() {} + unsafe fn flush() {} + unsafe fn release() {} + unsafe fn write(_bytes: &[u8]) {} + } + + defmt::timestamp!(""); +} + +#[test] +fn defmt() { + #[bitfield(u64, defmt = true)] + struct Full { + data: u64, + } + + let full = Full::new().with_data(123); + defmt::println!("{:?}", full); +} + +#[test] +fn defmt_primitives() { + #[bitfield(u128, defmt = true)] + struct Unsigned { + a: bool, + #[bits(7)] + __: u8, + b: u8, + c: u16, + d: u32, + e: u64, + } + + defmt::println!("{}", Unsigned::new()); + + #[bitfield(u128, defmt = true)] + struct FullUnsigned { + data: u128, + } + + defmt::println!("{}", FullUnsigned::new()); + + #[bitfield(u128, defmt = true)] + struct Signed { + a: bool, + #[bits(7)] + __: u8, + b: i8, + c: i16, + d: i32, + e: i64, + } + + defmt::println!("{}", Signed::new()); + + #[bitfield(u128, defmt = true)] + struct FullSigned { + data: i128, + } + + defmt::println!("{}", FullSigned::new()); + + #[bitfield(u128, defmt = true)] + struct Size { + #[bits(64)] + a: usize, + #[bits(64)] + b: usize, + } + + defmt::println!("{}", Size::new()); + + const fn f32_from_bits(_bits: u32) -> f32 { + // just for testing + 0.0 + } + + const fn f64_from_bits(_bits: u64) -> f64 { + // just for testing + 0.0 + } + + #[bitfield(u128, defmt = true)] + struct Float { + __: u32, + #[bits(32, from = f32_from_bits, access = RO)] + a: f32, + #[bits(64, from = f64_from_bits, access = RO)] + b: f64, + } + + defmt::println!("{}", Float::new()); +} + #[test] fn positive() { #[bitfield(u32)]