Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/test-debug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features
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
12 changes: 11 additions & 1 deletion .github/workflows/test-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: test
args: --release --no-default-features
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
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ members = [
"bitbybit",
"bitbybit-tests",
]

exclude = [
"test-no-std",
]
17 changes: 17 additions & 0 deletions bitbybit-tests/src/bitfield_tests.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 }");
}
23 changes: 23 additions & 0 deletions bitbybit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
36 changes: 36 additions & 0 deletions bitbybit/examples/simple.rs
Original file line number Diff line number Diff line change
@@ -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());
}
2 changes: 1 addition & 1 deletion bitbybit/src/bitfield/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TokenStream2>) {
Expand Down
164 changes: 98 additions & 66 deletions bitbybit/src/bitfield/mod.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<TokenStream2> = None;
#[derive(Default)]
struct BitfieldAttributes {
pub base_type: Option<Ident>,
pub default_val: Option<DefaultVal>,
pub debug_trait: bool,
}

enum ArgumentType {
Default,
}
let mut next_expected: Option<ArgumentType> = None;

fn handle_next_expected(
next_expected: &Option<ArgumentType>,
default_value: &mut Option<TokenStream2>,
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::<Token![:]>().is_err() && stream.parse::<Token![=]>().is_err() {
return Err(syn::Error::new(
meta.input.span(),
"Expected `:` or `=` after `default`",
));
}
let lit_int: Result<LitInt, syn::Error> = 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<Ident, syn::Error> = 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
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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<TokenStream2> = 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,
Expand Down Expand Up @@ -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());
Expand Down
2 changes: 2 additions & 0 deletions test-no-std/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
2 changes: 2 additions & 0 deletions test-no-std/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
/Cargo.lock
7 changes: 7 additions & 0 deletions test-no-std/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "test-no-std"
version = "0.1.0"
edition = "2024"

[dependencies]
bitbybit = { path = "../bitbybit" }
13 changes: 13 additions & 0 deletions test-no-std/src/main.rs
Original file line number Diff line number Diff line change
@@ -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 {}
}