diff --git a/.github/workflows/test-debug.yml b/.github/workflows/test-debug.yml index 345cfc1..0cf9bc9 100644 --- a/.github/workflows/test-debug.yml +++ b/.github/workflows/test-debug.yml @@ -15,4 +15,14 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test - args: --no-default-features \ No newline at end of file + args: --no-default-features + + build-no-std: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + target: + - thumbv7em-none-eabihf + - run: cargo build --manifest-path ./test-no-std/Cargo.toml --target thumbv7em-none-eabihf --no-default-features diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index 63f0dec..ca14ee5 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -15,4 +15,14 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test - args: --release --no-default-features \ No newline at end of file + args: --release --no-default-features + + build-no-std: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + target: + - thumbv7em-none-eabihf + - run: cargo build --release --manifest-path ./test-no-std/Cargo.toml --target thumbv7em-none-eabihf --no-default-features diff --git a/Cargo.toml b/Cargo.toml index 15a6e65..b9bf9ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,6 @@ members = [ "bitbybit", "bitbybit-tests", ] - +exclude = [ + "test-no-std", +] diff --git a/bitbybit-tests/src/bitfield_tests.rs b/bitbybit-tests/src/bitfield_tests.rs index e0a07ae..29543aa 100644 --- a/bitbybit-tests/src/bitfield_tests.rs +++ b/bitbybit-tests/src/bitfield_tests.rs @@ -1,4 +1,6 @@ use arbitrary_int::Number; +use std::fmt::Debug; + use arbitrary_int::{u1, u12, u13, u14, u2, u24, u3, u30, u4, u48, u5, u57, u7}; use bitbybit::bitenum; use bitbybit::bitfield; @@ -1661,3 +1663,18 @@ fn test_fully_qualified_paths() { assert_eq!(t.exhaustive_enum(), inner::TestEnum::Var2); assert_eq!(t.non_exhaustive_enum(), Ok(inner::TestEnum2::Var1)); } + +#[test] +fn test_debug_impl() { + #[bitfield(u16, debug)] + struct Test { + #[bits(8..=15, rw)] + upper: u8, + + #[bits(0..=7, rw)] + lower: u8, + } + let test = Test::new_with_raw_value(0x1F2F); + let display_str = format!("{:?}", test); + assert_eq!(display_str, "Test { upper: 31, lower: 47 }"); +} diff --git a/bitbybit/README.md b/bitbybit/README.md index d24c6bb..a616537 100644 --- a/bitbybit/README.md +++ b/bitbybit/README.md @@ -187,6 +187,29 @@ immediates in a way that they have to be reassembled. This can be achieved like } ``` +## Debug + +The `bitfield` macro can generate a `Debug` implementation for you which prints +the `Debug` implementation of the inner fields. You can do this using the `debug` +specifier: + +```rs +#[bitfield(u32, debug)] +struct GICD_TYPER { + #[bits(11..=15, r)] + lspi: u5, + + #[bit(10, r)] + security_extn: bool, + + #[bits(5..=7, r)] + cpu_number: u3, + + #[bits(0..=4, r)] + itlines_number: u5, +} +``` + ## Dependencies Arbitrary bit widths like u5 or u67 do not exist in Rust at the moment. Therefore, the following dependency is required: diff --git a/bitbybit/examples/simple.rs b/bitbybit/examples/simple.rs new file mode 100644 index 0000000..e9201c0 --- /dev/null +++ b/bitbybit/examples/simple.rs @@ -0,0 +1,36 @@ +use arbitrary_int::u4; +use bitbybit::bitfield; + +#[bitfield(u32, debug)] +pub struct BitfieldU32 { + #[bits(28..=31, rw)] + val3: u4, + #[bits(24..=27, rw)] + val2: u4, + #[bits(16..=23, rw)] + val1: u8, + #[bits(0..=15, rw)] + val0: u16, +} + +pub fn main() { + let bitfield = BitfieldU32::new_with_raw_value(0x0); + println!("Bitfield with zero values: {:?}", bitfield); + println!("Field 0: {}", bitfield.val0()); + println!("Field 1: {}", bitfield.val1()); + println!("Field 2: {}", bitfield.val2()); + println!("Field 3: {}", bitfield.val3()); + println!("Raw value: {}", bitfield.raw_value()); + + let bitfield_with_values = BitfieldU32::new_with_raw_value(0x1234_5678); + + println!( + "Bitfield, with value 0x1234_5678: {:?}", + bitfield_with_values + ); + println!("Field 0: {:#x}", bitfield_with_values.val0()); + println!("Field 1: {:#x}", bitfield_with_values.val1()); + println!("Field 2: {:#x}", bitfield_with_values.val2()); + println!("Field 3: {:#x}", bitfield_with_values.val3()); + println!("Raw value: {:#x}", bitfield_with_values.raw_value()); +} diff --git a/bitbybit/src/bitfield/codegen.rs b/bitbybit/src/bitfield/codegen.rs index e135d2d..25b0588 100644 --- a/bitbybit/src/bitfield/codegen.rs +++ b/bitbybit/src/bitfield/codegen.rs @@ -353,7 +353,7 @@ pub fn make_builder( has_default: bool, struct_vis: &Visibility, internal_base_data_type: &Type, - base_data_type: &TokenTree, + base_data_type: &Ident, base_data_size: BaseDataSize, field_definitions: &[FieldDefinition], ) -> (TokenStream2, Vec) { diff --git a/bitbybit/src/bitfield/mod.rs b/bitbybit/src/bitfield/mod.rs index e1f92c8..8f72d12 100644 --- a/bitbybit/src/bitfield/mod.rs +++ b/bitbybit/src/bitfield/mod.rs @@ -1,13 +1,16 @@ mod codegen; mod parsing; +use proc_macro::Span; use proc_macro::TokenStream; +use proc_macro2::Ident; use proc_macro2::TokenStream as TokenStream2; -use proc_macro2::{Ident, TokenTree}; +use quote::TokenStreamExt; use quote::{quote, ToTokens}; use std::ops::Range; use std::str::FromStr; -use syn::{Attribute, Data, DeriveInput, Type}; +use syn::meta::ParseNestedMeta; +use syn::{parse_macro_input, Attribute, Data, DeriveInput, LitInt, Token, Type}; /// In the code below, bools are considered to have 0 bits. This lets us distinguish them /// from u1 @@ -73,78 +76,82 @@ impl BaseDataSize { } } -pub fn bitfield(args: TokenStream, input: TokenStream) -> TokenStream { - let args: Vec<_> = proc_macro2::TokenStream::from(args).into_iter().collect(); +pub enum DefaultVal { + Lit(LitInt), + Constant(Ident), +} - if args.is_empty() { - panic!( - "bitfield! No arguments given, but need at least base data type (e.g. 'bitfield(u32)')" - ); +impl ToTokens for DefaultVal { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + DefaultVal::Lit(lit) => lit.to_tokens(tokens), + DefaultVal::Constant(ident) => ident.to_tokens(tokens), + } } +} - // Parse arguments: the first argument is required and has the base data type. Further arguments are - // optional and are key:value pairs - let base_data_type = &args[0]; - let mut default_value: Option = None; +#[derive(Default)] +struct BitfieldAttributes { + pub base_type: Option, + pub default_val: Option, + pub debug_trait: bool, +} - enum ArgumentType { - Default, - } - let mut next_expected: Option = None; - - fn handle_next_expected( - next_expected: &Option, - default_value: &mut Option, - token_stream: TokenStream2, - ) { - match next_expected { - None => panic!("bitfield!: Unexpected token {}. Example of valid syntax: #[bitfield(u32, default = 0)]", token_stream), - Some(ArgumentType::Default) => { - *default_value = Some(token_stream); - } +impl BitfieldAttributes { + fn parse(&mut self, meta: ParseNestedMeta, index: usize) -> Result<(), syn::Error> { + if index == 0 { + self.base_type = Some(meta.path.require_ident()?.clone()); + return Ok(()); } - } - for arg in args.iter().skip(1) { - match arg { - TokenTree::Punct(p) => match p.as_char() { - ',' => next_expected = None, - '=' | ':' => (), - _ => panic!( - "bitfield!: Expected ',', '=' or ':' in argument list. Saw '{}'", - p - ), - }, - TokenTree::Ident(sym) => { - if next_expected.is_some() { - // We might end up here if we refer to a constant, like 'default = SOME_CONSTANT' - handle_next_expected(&next_expected, &mut default_value, sym.to_token_stream()); - } else { - match sym.to_string().as_str() { - "default" => { - if default_value.is_some() { - panic!("bitfield!: default must only be specified at most once"); - } - next_expected = Some(ArgumentType::Default) - } - _ => panic!( - "bitfield!: Unexpected argument {}. Supported: 'default'", - sym - ), - } - } + if meta.path.is_ident("default") { + let stream = &meta.input; + + // Try parsing either `:` or `=` + if stream.parse::().is_err() && stream.parse::().is_err() { + return Err(syn::Error::new( + meta.input.span(), + "Expected `:` or `=` after `default`", + )); + } + let lit_int: Result = stream.parse(); + if lit_int.is_ok() { + self.default_val = Some(DefaultVal::Lit(lit_int.unwrap())); + return Ok(()); } - TokenTree::Literal(literal) => { - // We end up here if we see a literal, like 'default = 0x1234' - handle_next_expected( - &next_expected, - &mut default_value, - literal.to_token_stream(), - ); + let path: Result = stream.parse(); + if path.is_ok() { + self.default_val = Some(DefaultVal::Constant(path.unwrap())); + return Ok(()); } - t => panic!("bitfield!: Unexpected token {}. Example of valid syntax: #[bitfield(u32, default = 0)]", t), + return Ok(()); } + if meta.path.is_ident("debug") { + self.debug_trait = true; + return Ok(()); + } + Ok(()) + } +} + +pub fn bitfield(args: TokenStream, input: TokenStream) -> TokenStream { + let mut bitfield_attrs = BitfieldAttributes::default(); + let mut index = 0; + let bitfield_parser = syn::meta::parser(|meta| { + let result = bitfield_attrs.parse(meta, index); + index += 1; + result + }); + if args.is_empty() { + return syn::Error::new( + Span::call_site().into(), + "bitfield! No arguments given, but need at least a base data type (e.g. 'bitfield(u32)')").to_compile_error().into(); } + parse_macro_input!(args with bitfield_parser); + if bitfield_attrs.base_type.is_none() { + panic!("bitfield!: First argument must be the base data type, e.g. 'bitfield(u32)'",); + } + let base_data_type = bitfield_attrs.base_type.as_ref().unwrap(); // If an arbitrary-int is specified as a base-type, we only use that when exposing it // (e.g. through raw_value() and for bounds-checks). The actual raw_value field will be the next // larger integer field @@ -184,7 +191,10 @@ pub fn bitfield(args: TokenStream, input: TokenStream) -> TokenStream { }; let accessors = codegen::generate(&field_definitions, base_data_size, &internal_base_data_type); - let (default_constructor, default_trait) = if let Some(default_value) = default_value.clone() { + let (default_constructor, default_trait) = if let Some(default_value) = + &bitfield_attrs.default_val + { + let default_value = default_value.to_token_stream(); let constructor = { let comment = format!("An instance that uses the default value {}", default_value); let deprecated_warning = format!( @@ -224,9 +234,30 @@ pub fn bitfield(args: TokenStream, input: TokenStream) -> TokenStream { (quote! {}, quote! {}) }; + let mut debug_trait = TokenStream2::new(); + if bitfield_attrs.debug_trait { + let debug_fields: Vec = field_definitions + .iter() + .map(|field| { + let field_name = &field.field_name; + quote! { + .field(stringify!(#field_name), &self.#field_name()) + } + }) + .collect(); + debug_trait.append_all(quote! { + impl ::core::fmt::Debug for #struct_name { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(#struct_name)) + #(#debug_fields)* + .finish() + } + } + }); + } let (new_with_constructor, new_with_builder_chain) = codegen::make_builder( &struct_name, - default_value.is_some(), + bitfield_attrs.default_val.is_some(), &struct_vis, &internal_base_data_type, base_data_type, @@ -285,6 +316,7 @@ pub fn bitfield(args: TokenStream, input: TokenStream) -> TokenStream { #( #accessors )* } #default_trait + #debug_trait #( #new_with_builder_chain )* }; //println!("Expanded: {}", expanded.to_string()); diff --git a/test-no-std/.cargo/config.toml b/test-no-std/.cargo/config.toml new file mode 100644 index 0000000..f9d2422 --- /dev/null +++ b/test-no-std/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU) diff --git a/test-no-std/.gitignore b/test-no-std/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/test-no-std/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/test-no-std/Cargo.toml b/test-no-std/Cargo.toml new file mode 100644 index 0000000..ba522f5 --- /dev/null +++ b/test-no-std/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "test-no-std" +version = "0.1.0" +edition = "2024" + +[dependencies] +bitbybit = { path = "../bitbybit" } diff --git a/test-no-std/src/main.rs b/test-no-std/src/main.rs new file mode 100644 index 0000000..981546c --- /dev/null +++ b/test-no-std/src/main.rs @@ -0,0 +1,13 @@ +#![no_std] +#![no_main] + +#[bitbybit::bitfield(u32, debug)] +pub struct TestReg { + #[bits(0..=31, rw)] + value: u32, +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}