| Crates.io | abstract-bits |
| lib.rs | abstract-bits |
| version | 0.2.0 |
| created_at | 2025-05-11 21:20:50.350557+00 |
| updated_at | 2025-05-25 09:20:16.389504+00 |
| description | Turn any combination of bit and byte fields into a structs |
| homepage | |
| repository | https://github.com/dvdsk/abstract-bits |
| max_upload_size | |
| id | 1669775 |
| size | 48,251 |
Turn combinations of bit and byte fields into a structs, even if they represent Options and Lists.
Let's jump directly into an example. You want to map a zigbee Link Status Command to a high level rust struct. The frame's shape changes depending on some bits.
This is the spec for the frame:
Bit: | 0 – 4 | 5 | 6 | 7 | 8 -
| List length | First frame | Last frame | Reserved | Link status list
Each link status is:
Bit: | 0 – 15 | 16-18 | 19 | 20-22 | 23
| Neighbor network address | Incoming cost | Reserved | Outgoing cost | Reserved
Its especially tricky that an earlier bitfield is determining the list length. Not even a hacky combination of serde and a bitfield crate can generate (de)-serialize code for us.
Which is why we now have abstract-bits!
use abstract_bits::{abstract_bits, AbstractBits};
#[abstract_bits]
struct LinkStatusCommand {
#[abstract_bits(length_of = link_statuses)]
reserved: u5,
is_first_frame: bool,
is_last_frame: bool,
reserved: u1,
link_statuses: Vec<LinkStatus>,
}
#[abstract_bits]
struct LinkStatus {
neighbor_address: u16,
incoming_cost: u3,
reserved: u1,
outgoing_cost: u3,
reserved: u1,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let bytes = LinkStatusCommand {
is_first_frame: false,
is_last_frame: true,
link_statuses: Vec::new(),
}.to_abstract_bits()?;
let link_status_cmd = LinkStatusCommand::from_abstract_bits(&bytes)?;
print!("number of links: {}", link_status_cmd.link_statuses.len());
Ok(())
}
#[abstract-bits] above your struct and any derives.u<n> (n a natural number larger than zero) for numeric fields. In the
transformed struct these will transform to the smallest rust primitives that
can represent them. For example an u7 will become an u8.reserved = u<n>.Option field place #[abstract-bits(presence_of = <field_name>)]
above the reserved: bool fields which controls whether the Option is
Some or None.Vec field place #[abstract-bits(length_of = <field_name>)]
above the reserved: u<n> fields which controls the length of the Vec.#[abstract-bits(bits = <N>)] above your enum. Replace N with the
number of bits the enum should occupy when serialized. Make sure any derives
follow after.#[repr(<Type>] attribute, for example #[repr(u8)].use abstract_bits::{abstract_bits, AbstractBits, BitReader};
// The size of this is:
// - 4+1+5+2+2|0+n*18, with n in range 0..u5::MAX
// so this is at most 14 + 31*18 = 572 bits long
#[abstract_bits]
#[derive(Debug, PartialEq, Eq)] // note: derives follow after
struct Frame {
header: u4,
#[abstract_bits(presence_of = source)]
reserved: bool,
#[abstract_bits(length_of = data)]
reserved: u5,
frame_type: Type,
source: Option<u16>,
data: Vec<Message>,
}
/// This is: 4+3+1+10 = 18 bits long
#[abstract_bits]
#[derive(Debug, PartialEq, Eq)]
struct Message {
header: u4,
reserved: u3,
is_important: bool,
bits: [bool; 4]
}
#[abstract_bits(bits = 2)]
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
enum Type {
#[default]
System = 0,
Personal = 1,
Group = 2,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let bytes = Frame {
header: 12,
frame_type: Type::default(),
source: Some(4243),
data: vec![Message {
header: 9,
is_important: false,
bits: [true, false, true, true]
}],
}.to_abstract_bits()?;
let mut reader = BitReader::from(bytes.as_slice());
let mut frame = Frame::read_abstract_bits(&mut reader)?;
if frame.frame_type == Type::default() {
for message in &mut frame.data {
message.is_important = true;
}
}
let bytes = frame.to_abstract_bits();
Ok(())
}
no-std & no-alloc support (quite trivial)HashMap/HashSet/BtreeMap support