chapa
Bitfield structs, batteries included!
chapa exposes an attribute macro, #[bitfield], that turns an ordinary
struct into a newtype backed by a single primitive. Every field maps to an exact
range of bits and gets a generated getter, setter, and with_* builder. A
companion attribute macro, #[bitenum], makes a C-like enum usable as a field
type.
Features
- MSB0 and LSB0 support: Naturally write bit orders as per datasheet
- Signed fields:
i8...i128field types with automatic sign extension - Enum fields: Use enums as bitfield fields with
#[bitenum] - Nested bitfields: Embed one bitfield struct inside another
- Readonly fields: Suppress setter generation with
readonlyor a leading_prefix - Default values: Set a field's initial value with
default = ... - Aliases: Expose extra accessor names with
alias = "name"oralias = ["a", "b"] - Overlays: Allow multiple logically distinct field groups to share the same bit range
- Bitwise operators:
&,|,^,!,&=,|=,^=with the backing storage type work directly on the struct - Raw arithmetic:
wrapping_,saturating_,checked_, andoverflowing_variants ofadd/subon the raw storage value - Bit extraction:
extract_bits!masks a value to keep only the specified bit ranges - Bit insertion:
place_bits!shifts a value into a range,insert_bits!merges already-positioned bits - Reflection: Opt into the
reflectionfeature for compile-time field metadata (FIELDS, bit positions, enum variants)
MSRV
Requires Rust 1.83 or newer (the generated getters, setters, and with_*
builders are const fn).
Quick start
use bitfield;
// An 8-bit status register, bit 0 is the LSB
let r = zeroed
.with_enabled
.with_mode;
assert!;
assert_eq!;
assert_eq!; // accessible as `reserved`, not `_reserved`
#[bitfield(...)] options
| Option | Required | Description |
|---|---|---|
u8 / u16 / u32 / u64 / u128 |
Yes | Backing storage type |
order = msb0 / order = lsb0 |
Yes | Bit numbering convention |
width = N |
No | Effective logical width, must be <= storage width |
#[bits(...)] options
| Option | Description |
|---|---|
N |
Single bit at index N |
N..=M |
Inclusive range from bit N to bit M |
N..M |
Half-open range (equivalent to N..=(M-1)) |
readonly |
Suppress set_* and with_* generation |
default = <expr> |
Starting value applied by default() |
alias = "name" |
Generate additional accessor under name |
alias = ["a","b"] |
Multiple aliases |
overlay = "group" |
Allow overlap with fields in other overlay groups |
A field's type may be bool (single bit), an unsigned integer (u8...u128),
a signed integer (i8...i128, two's-complement: sign-extended on read,
truncated to the field width on write), a #[bitenum] enum, or another
bitfield struct.
MSB-0 example
use bitfield;
// A 32-bit value where bit 0 is the most-significant bit
let cw = zeroed
.with_opcode
.with_dst;
assert_eq!;
Enum fields
Use #[bitenum] on an enum to implement BitField, allowing it to be
used as a bitfield field type. The enum must mark exactly one variant
#[fallback].
use ;
let dc = zeroed
.with_enable
.with_fmt;
assert_eq!;
// Unrecognized raw values are handled two ways:
// - from_raw (and the getter dc.fmt()) coerce them to the #[fallback] variant
// - try_from_raw / TryFrom reject them, so corrupt input can be detected
assert_eq!; // coerced to #[fallback]
assert!; // detected
assert_eq!; // TryFrom<u8>
Nested bitfields
A field whose type implements chapa::BitField (i.e. any type annotated with
#[bitfield]) can be used as a nested field.
use bitfield;
let nibble = zeroed.with_high.with_low;
let word = zeroed.with_top.with_bottom;
assert_eq!;
assert_eq!;
assert_eq!;
Overlay groups
Fields in different overlay groups may share bit ranges. This is useful for instruction formats where the same bits are interpreted differently depending on other bits. This is useful for instruction decoding, but also to handle specific MMIO registers that change their meaning depending on certain encoded bits.
use bitfield;
let r_form = zeroed.with_opcode.with_rs.with_ra.with_rb;
assert_eq!;
let i_form = zeroed.with_opcode.with_dst.with_imm;
assert_eq!;
assert_eq!; // Both names cover bits 6..=10
Constructors and default values
Every struct has a const fn zeroed() that returns an all-zero value. There is
no new(). Add default = <expr> to give a field a different initial value.
This automatically implements Default. The zeroed() and from_raw()
methods do not apply field defaults. If no fields have defaults, you can still
use #[derive(Default)] to make default() return zeroed().
default works on any field type (bool, integer, #[bitenum] enum,
or nested bitfield, e.g. default = Mode::On), including readonly ones.
Values wider than the field truncate to its width, exactly like a setter.
use bitfield;
let c = default;
assert_eq!;
assert_eq!;
assert_eq!; // no default -> zero
// zeroed() and from_raw never inject defaults
assert_eq!;
assert_eq!;
Bitwise operations
Every bitfield struct implements BitAnd, BitOr, BitXor, Not,
BitAndAssign, BitOrAssign, and BitXorAssign. The right-hand operand may be
the raw storage type or any bitfield backed by the same storage type; the result
keeps the left-hand bitfield type.
use bitfield;
const HIGH_BYTE: u32 = 0xFF00_0000;
let current = from_raw;
let incoming: u32 = 0xABCD_EF01;
// Keep the low 24 bits of `current`, replacing only its high byte.
let updated = | ;
assert_eq!;
assert!;
assert_eq!;
Raw arithmetic
Every bitfield struct provides wrapping_add, wrapping_sub, saturating_add,
saturating_sub, checked_add, checked_sub, overflowing_add, and
overflowing_sub, mirroring the methods on the backing storage type. They
operate on the full raw storage value, exactly like raw(): carries and
borrows propagate across field boundaries, and bit ordering plays no role.
use bitfield;
let c = from_raw.wrapping_add;
assert_eq!;
// Carries cross field boundaries, just like on the raw integer.
let c = from_raw.wrapping_add;
assert_eq!;
assert_eq!;
assert_eq!;
let = from_raw.overflowing_sub;
assert_eq!;
assert!;
If you need wrap-around at a single field's width instead, the setters already
truncate to the field width, so c.set_low(c.low().wrapping_add(1)) wraps
correctly within low alone.
Bit extraction
extract_bits! keeps only the specified bit positions from a value, zeroing all others.
Bits can be single indices, inclusive start..=end ranges, or half-open
start..end ranges. Indices and ranges may be runtime expressions.
For raw integers, specify the ordering and type explicitly:
use extract_bits;
let val: u32 = 0xFFFF_FFFF;
// MSB0: keep bits 0, 5–9, 16–31
let masked = extract_bits!;
assert_eq!;
// LSB0: keep bits 0–3 and 12–15
let masked = extract_bits!;
assert_eq!;
// Runtime ranges work too.
let offset = 8u8;
let masked = extract_bits!;
assert_eq!;
For chapa bitfield structs, omit the ordering. It is deduced from the struct's
#[bitfield] definition, and the result has the same struct type:
use ;
let packet = from_raw;
let masked: Packet = extract_bits!;
assert_eq!;
The explicit form (msb0 u32) remains usable in const contexts when its value
and bit specs are literals. Runtime specs are evaluated when the macro is
called. The struct form calls an #[inline] helper and has no language-level
const guarantee.
Bit insertion
These macros update bit ranges without using field setters:
place_bits!shifts a right-aligned value into one bit or range.insert_bits!copies already-positioned bits into one or more ranges.
Both macros support explicit msb0 and lsb0 forms. With a bitfield value, the
ordering is inferred. They return the updated value instead of changing it in
place. Bit indices and ranges may be runtime expressions.
use ;
// Write 0xAB to bits 8..=15.
let mut reg = from_raw;
reg = place_bits!;
assert_eq!;
assert_eq!;
// Replace the low two bytes with already-positioned bits.
reg = insert_bits!;
assert_eq!;
The explicit forms remain const-evaluable with literal specs. For an msb0
bitfield with width = N, use the explicit form because the inferred form uses
the full storage width.
Reflection
Enable the reflection feature to get compile-time field metadata for every
#[bitfield] struct and #[bitenum] enum:
[]
= { = "0.9", = ["reflection"] }
Each bitfield struct gains an inherent FIELDS: &'static [FieldInfo] const
describing its fields: their accessor name, bit position, aliases and how the
raw bits should be interpreted. Offsets and widths are physical (in
storage-value "coordinates"), so a field's value is always
(raw >> offset) & ((1 << width) - 1) regardless of msb0/lsb0 ordering.
Nested enum and struct fields carry their own variant table / fields.
use ;
let mode = FIELDS.iter.find.unwrap;
assert_eq!;
assert_eq!;
if let Enum = mode.kind else
FieldKind distinguishes Bool, Uint, Sint, Enum(&EnumInfo) and
Struct(&[FieldInfo]). The types (FieldInfo, FieldKind, EnumInfo) and the
Reflect trait are re-exported at the crate root when the feature is on.
Generated API
For a field foo: u8 spanning bits 4..=7 the macro generates:
| Item | Signature |
|---|---|
| Constant | pub const FOO_SHIFT: u32 |
| Constant | pub const FOO_MASK: StorageType |
| Getter | pub const fn foo(&self) -> u8 |
| Setter | pub const fn set_foo(&mut self, val: u8) |
| Builder | pub const fn with_foo(self, val: u8) -> Self |
Every struct also provides these methods (N is the storage size in bytes):
| Item | Signature |
|---|---|
| Zeroed | pub const fn zeroed() -> Self |
| Raw access | pub const fn from_raw(val: StorageType) -> Self |
| Raw access | pub const fn raw(&self) -> StorageType |
| Bytes | pub const fn to_le_bytes(self) -> [u8; N] |
| Bytes | pub const fn to_be_bytes(self) -> [u8; N] |
| Bytes | pub const fn to_ne_bytes(self) -> [u8; N] |
| Bytes | pub const fn from_le_bytes(bytes: [u8; N]) -> Self |
| Bytes | pub const fn from_be_bytes(bytes: [u8; N]) -> Self |
| Bytes | pub const fn from_ne_bytes(bytes: [u8; N]) -> Self |
| Arithmetic | pub const fn wrapping_add(self, rhs: StorageType) -> Self (same shape for wrapping_sub, saturating_add, saturating_sub) |
| Arithmetic | pub const fn checked_add(self, rhs: StorageType) -> Option<Self> (same shape for checked_sub) |
| Arithmetic | pub const fn overflowing_add(self, rhs: StorageType) -> (Self, bool) (same shape for overflowing_sub) |
The byte conversions and arithmetic methods operate on the full storage value,
matching raw() and from_raw().
Additionally, every struct implements the following traits:
| Trait | Signature |
|---|---|
BitAnd |
fn bitand<Rhs>(self, rhs: Rhs) -> Self |
BitOr |
fn bitor<Rhs>(self, rhs: Rhs) -> Self |
BitXor |
fn bitxor<Rhs>(self, rhs: Rhs) -> Self |
Not |
fn not(self) -> Self |
BitAndAssign |
fn bitand_assign<Rhs>(&mut self, rhs: Rhs) |
BitOrAssign |
fn bitor_assign<Rhs>(&mut self, rhs: Rhs) |
BitXorAssign |
fn bitxor_assign<Rhs>(&mut self, rhs: Rhs) |
Rhs may be the backing storage type or a bitfield with the same backing
storage type.