Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
5 changes: 4 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Run tests
- name: Run crate tests
run: cargo test

- name: Run macro doc-tests
run: cargo test --manifest-path macros/Cargo.toml
125 changes: 110 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# bitmap
# `bitmap!`

Procedural macro for defining bitmap structs packed as tightly as possible.
Generates a packed bitmap newtype struct with field-level bit access.

- Define fields with `u1` to `u7` widths
- Up to 64 bits total per struct (64 `u1`s, 9 `u7`s + 1 `u1`, etc.)
- Getters and setters are generated
The macro expands to a newtype struct around a `u8` to `u128`, depending on the total bit width
of the definition, with automatically generated getters and setters for each field.

## Example
## API

### Usage Example

```rust
use bitmap::bitmap;
Expand All @@ -19,16 +20,110 @@ bitmap!(
}
);

let mut p = Player(0);
p.set_imposter(1);
p.set_finished_tasks(5);
p.set_kills(3);

assert_eq!(p.finished_tasks(), 5);
let mut player = Player(0);
assert_eq!(std::mem::size_of::<Player>(), 1);

player.set_imposter(1);
player.set_finished_tasks(5);
player.set_kills(3);

assert_eq!(player.imposter(), 1);
assert_eq!(player.finished_tasks(), 5);
assert_eq!(player.kills(), 3);
assert_eq!(*player, 0b01101011);
```

# Limitations
### Accessing fields

For each field `name: T`, where `T` is the smallest possible integer such that
`field_size <= integer.size`, `bitmap!` generates:

- `fn name(&self) -> T` — returns the value for `name`
- `fn set_name(&mut self, val: T)` — sets the value for `name`

### Accessing the raw value

For the struct `Bits(T)`, where `T` is the unsigned integer type used for storage,
the following traits are implemented:

- `From<Bits> for T`
- `Deref for Bits`, with `fn deref(&self) -> T`

```rust
use bitmap::bitmap;

bitmap!(
struct Bits {
a: u32,
b: u16,
c: u16,
}
);

let bits = Bits(0);
let underlying_u64: u64 = bits.into();
let underlying_u64 = *bits;
```

### Supported field types:

```rust
use bitmap::bitmap;

bitmap!(
struct Bits {
flag: u1,
counter: u7,
}
);
```

* Total struct size must be ≤ 64 bits (more planned)
* Only `u1` to `u7` types are currently supported (more planned)
Each field must be in the form `uN`, where `1 <= N <= 128`.

### Maximum total size

`bitmap!` uses the smallest possible integer type such that `total_bit_width <= integer.bit_width`.
The total bit width must fit into a `u128`. If you need more than that, consider using a `Vec`
of `bitmap`s.

### Storage order

Fields are packed from **most significant bit (MSB)** to **least significant bit (LSB)**, matching
big-endian order.

This means the first declared field is stored in the highest bits of the underlying storage integer.

```rust
use bitmap::bitmap;

bitmap!(
struct Bits {
a: u8,
b: u8,
}
);

let mut bits = Bits(0);
bits.set_a(0xaa)
.set_b(0xbb);

assert_eq!(*bits, 0xaabb);
```

## Note

`bitmap!` is built with hardware configuration in mind, where most packed bitmaps have a size
aligned to integer sizes. It does not use the _smallest possible size_: a bitmap with only one `u33`
field will take up 64 bits of space.

```rust
use bitmap::bitmap;

bitmap!(
struct Bits {
field: u33,
}
);

assert_eq!(core::mem::size_of::<Bits>(), 8);
```
16 changes: 16 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use std::{fs::File, io::Write, path::Path};

fn main() {
let out_dir = std::env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("generated_tests.rs");
let mut f = File::create(&dest_path).unwrap();

for n in 1..=128 {
let max_val = if n != 128 {
(1u128 << n) - 1
} else {
340282366920938463463374607431768211455
};
writeln!(f, "test_width!(u{n}, {max_val});").unwrap();
}
}
78 changes: 62 additions & 16 deletions macros/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,70 @@ use quote::quote;

use crate::parser::BitmapInput;

pub fn expand_bitmap(input: BitmapInput) -> syn::Result<TokenStream2> {
let name = &input.name;
let fields = &input.fields;
let size = input.fields.iter().map(|f| f.size).sum();
fn get_packed_layout(size: usize) -> Vec<u8> {
let usizes = [128, 64, 32, 16, 8];
let mut remainder = size;
let mut sizes = Vec::<u8>::new();

for &usz in &usizes {
while remainder >= usz as usize {
sizes.push(usz);
remainder -= usz as usize;
}
}

if size > 64 {
return Err(syn::Error::new_spanned(name, "Too many fields: max supported size is 64 bits"));
if remainder > 0 {
sizes.push(8);
}

let storage_ty = match size {
sizes
}

fn get_storage_ty(size: u8) -> TokenStream2 {
match size {
0..=8 => quote! { u8 },
9..=16 => quote! { u16 },
17..=32 => quote! { u32 },
33..=64 => quote! { u64 },
65..=128 => quote! { u128 },
_ => unreachable!(),
};
}
}

let mut bit_index = 0;
pub fn expand_bitmap(input: BitmapInput) -> syn::Result<TokenStream2> {
let name = &input.name;
let fields = &input.fields;
let size: usize = input.fields.iter().map(|f| f.size as usize).sum();
let _packed_layout = get_packed_layout(size);

if size > 128 {
return Err(syn::Error::new_spanned(name, "Too many fields: maximum supported size is 128 bits"));
}

let storage_ty = get_storage_ty(size as u8);

let mut bit_index = size;
let accessors = fields.iter().map(|ident| {
let index: u8 = bit_index;
bit_index += ident.size;
bit_index -= ident.size as usize;
let index: usize = bit_index;
let setter_name = Ident::new(&format!("set_{}", ident.name), ident.name.span());
let name = ident.name.to_owned();
let size = ident.size;
let mask = quote! { ((0b1 << #size) - 1) as #storage_ty };
let this_storage_ty = get_storage_ty(size);
let mask = if size != 128 {
let mask_ty = get_storage_ty(size + 1);
quote! { (((0b1 as #mask_ty) << #size) - 1) as #storage_ty }
} else {
quote! { 340282366920938463463374607431768211455 }
};
quote! {
pub fn #name(&self) -> #storage_ty {
(self.0 >> #index) & #mask
#[inline]
pub const fn #name(&self) -> #this_storage_ty {
((self.0 >> #index) & #mask) as #this_storage_ty
}

pub fn #setter_name(&mut self, val: u8) -> &mut Self {
#[inline]
pub fn #setter_name(&mut self, val: #this_storage_ty) -> &mut Self {
self.0 = ((self.0 & !((#mask) << #index)) | (((val as #storage_ty) & #mask) << #index));
self
}
Expand All @@ -43,10 +76,23 @@ pub fn expand_bitmap(input: BitmapInput) -> syn::Result<TokenStream2> {
Ok(quote! {
#[derive(Debug, Clone, Copy)]
#[repr(transparent)]
pub struct #name(pub #storage_ty);
pub struct #name(#storage_ty);

impl #name {
#(#accessors)*
}

impl From<#name> for #storage_ty {
fn from(value: #name) -> Self {
value.0
}
}

impl core::ops::Deref for #name {
type Target = #storage_ty;
fn deref(&self) -> &Self::Target {
&self.0
}
}
})
}
104 changes: 88 additions & 16 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,13 @@ use syn::parse_macro_input;
mod generator;
mod parser;

/// Generates a bitmap struct from the given definition.
/// Generates a packed bitmap newtype struct with field-level bit access.
///
/// The macro expands to a newtype struct around a `u8` to `u64` (depending on the total size
/// of the definition), with automatically generated getters and setters for each field.
/// The macro expands to a newtype struct around a `u8` to `u128`, depending on the total bit width
/// of the definition, with automatically generated getters and setters for each field.
///
/// ### Supported field types
/// - `u1` through `u7`
///
/// ### Current Limitations
/// - Total bit size must be ≤ 64 bits.
/// - No values larger than `u7` currently supported
///
/// ### Generated API
/// For each field `name: T`, the macro generates:
/// - `fn name(&self) -> T` — returns the field value.
/// - `fn set_name(&mut self, val: T)` — sets the field value.
///
/// ### Example
/// ### API
/// #### Usage Example
/// ```
/// use macros::bitmap;
///
Expand All @@ -43,7 +32,90 @@ mod parser;
/// assert_eq!(player.imposter(), 1);
/// assert_eq!(player.finished_tasks(), 5);
/// assert_eq!(player.kills(), 3);
/// assert_eq!(*player, 0b01101011);
/// ```
/// #### Accessing fields
/// For each field `name: T`, where `T` is the smallest possible integer such that
/// `field_size <= integer.size`, `bitmap!` generates:
///
/// - `fn name(&self) -> T` — returns the value for `name`
/// - `fn set_name(&mut self, val: T)` — sets the value for `name`
///
/// #### Accessing the raw value
/// For the struct `Bits(T)`, where `T` is the unsigned integer type used for storage,
/// the following traits are implemented:
/// - `From<Bits> for T`
/// - `Deref for Bits`, with `fn deref(&self) -> T`
///
/// ```
/// use macros::bitmap;
///
/// bitmap!(
/// struct Bits {
/// a: u32,
/// b: u16,
/// c: u16,
/// }
/// );
///
/// let bits = Bits(0);
/// let underlying_u64: u64 = bits.into();
/// let underlying_u64 = *bits;
/// ```
/// ### Supported field types:
/// ```
/// use macros::bitmap;
///
/// bitmap!(
/// struct Bits {
/// flag: u1,
/// counter: u7,
/// }
/// );
/// ```
/// Each field must be in the form `uN`, where `1 <= N <= 128`.
/// ### Maximum total size
/// `bitmap!` uses the smallest possible integer type such that `total_bit_width <= integer.bit_width`.
/// The total bit width must fit into a `u128`. If you need more than that, consider using a `Vec`
/// of `bitmap`s.
/// ### Storage order
/// Fields are packed from **most significant bit (MSB)** to **least significant bit (LSB)**, matching
/// big-endian order.
///
/// This means the first declared field is stored in the highest bits of the underlying storage integer.
/// ```
/// use macros::bitmap;
///
/// bitmap!(
/// struct Bits {
/// a: u8,
/// b: u8,
/// }
/// );
///
/// let mut bits = Bits(0);
/// bits.set_a(0xaa)
/// .set_b(0xbb);
///
/// assert_eq!(*bits, 0xaabb);
/// ```
///
/// ### Note
/// `bitmap!` is built with hardware configuration in mind, where most packed bitmaps have a size
/// aligned to integer sizes. It does not use the _smallest possible size_: a bitmap with only one `u33`
/// field will take up 64 bits of space.
/// ```
/// use macros::bitmap;
///
/// bitmap!(
/// struct Bits {
/// field: u33,
/// }
/// );
///
/// assert_eq!(core::mem::size_of::<Bits>(), 8);
/// ```
///
#[proc_macro]
pub fn bitmap(input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as parser::BitmapInput);
Expand Down
Loading