tree: 90939d8edcf0cd51c1c8fee04d481c44f5b710b7
  1. bitrs-macro/
  2. examples/
  3. src/
  4. BUILD.gn
  5. OWNERS
  6. README.md
src/lib/bitrs/README.md

bitrs

bitrs (“bitters”) is a no-std crate for ergonomically specifying layouts of bitfields over integral types. While the aim is to be general-purpose, the imagined user is a systems programmer uncomfortably hunched over an architectural manual or hardware spec, looking to transcribe register layouts into Rust with minimal fuss.

The heavy lifting is done by the layout! procedural macro. Care is taken to generate readable and efficient code. The zerocopy crate is further leveraged for safe and efficient transmutation between integral values and custom bitfield representations.

Features

  • Generation of a simple, extensible wrapper type around the given integral base type;
  • Simple, const-friendly builder pattern for constructing layout instances;
  • Automatic implementations of the basic, convenient traits one would expect out of a thin integral wrapper type:
    • Copy, Clone
    • Eq, PartialEq
    • Default
    • From over the base type
    • Deref and DerefMut with a target of the underlying base type
    • Debug, Binary, LowerHex, UpperHex, Octal
  • Specification of default and “reserved-as” values, with new() respecting reserved-as values and default() respecting both;
  • Custom bitfield representation types without any boilerplate;
  • Iteration over individual bitfield metadata and values;
  • Associated constants around masks and shifts for use in inline assembly.

Example

use bitrs::{bitfield_repr, layout};

#[bitfield_repr(u8)]
pub enum CustomFieldRepr {
    Option1 = 0xa,
    Option2 = 0xf,
}

layout!({
    pub struct Example(u32);
    {
        let foo @ 21..14;
        let custom @ 13..10: CustomFieldRepr;
        let bar @ 9..8 = 0b11;
        let baz @ 7;
        let frob @ 6..4;
        let __ @ 3..2 = 1;
        let __ @ 1..0;
    }
});

fn main() {
    let example = *Example::default()
        .set_custom(CustomFieldRepr::Option2)
        .set_frob(0x7);
    assert_eq!(example.foo(), 0);
    assert_eq!(example.custom(), CustomFieldRepr::Option2);
    assert_eq!(example.bar(), 0b11);
    assert_eq!(example.baz(), false);
    assert_eq!(example.frob(), 0x7);
    assert_eq!(*example & 0b1100, 0b0100);

    // Will print: `Example { custom: Option2, foo: 0x0, bar: 0x3, baz: false, frob: 0xa }`
    println!("{example}");

    // Or iterate over all fields and and print them individually.
    for (metadata, value) in example {
      println!("{}: {:x}", metadata.name, value);
    }
}

Syntax

To keep the DSL intuitive and formattable, we co-opt a few familiar Rust syntax elements:

Despite the exclusive .. token, both endpoints of a BitRange are treated as inclusive bit indices; see Named and reserved fields.

Generated Code

Using the Example struct from the Example section, here is how the macro translates the definition:

Layout type

The layout type is always a tuple struct wrapping an unsigned integral type, giving a layout of bitfields over such an integer. The underlying integral type is referred to as the “base type”. In particular, Example defines a 32-bit layout.

Trait implementations

The basic, convenient traits one might expect of a thinly-wrapped integral type are implemented for the layout type:

  • Copy, Clone
  • Eq, PartialEq
  • Default (see below)
  • From over the base type
  • Deref and DerefMut, with a target of the base type
  • Debug, Binary, LowerHex, UpperHex, Octal

IntoIterator is also implemented to iterate over individual field values and metadata.

Layout type attributes

Though none are given on Example, all attributes annotating the layout type in the macro are forwarded verbatim to the definition. However, any derivations that conflict with the above implemented traits will result in a compilation error.

Visibility

To keep things simple and practical, only the visibility of the layout type may be specified; it too is forwarded verbatim from the definition in the macro. All methods are generated as public. The associated iterator type is also given the same visibility as the layout type.

Const-ness

All methods are const where possible. This is limited only by the current unavailability of const traits, the exceptions being trait methods and the getters and setters of fields with custom representations.

Named and reserved fields

Bitfields are defined in the block following the layout type definition, and each is defined with a let statement of one of the following forms:

  • let $name @ $bit (= $default)?;
  • let $name @ $high..$low (: $repr)? (= $default)?;
  • let __ @ $bit (= $value)?;
  • let __ @ $high..$low (= $value)?;

Reserved fields are denoted by the identifier __, and yield no accessors. A bare bit index $bit or range $high..$low (inclusive at both ends, despite the exclusive-range token) indicates the bits covered by the field. Fields that span a single bit are referred to as width-1 fields.

A width-1 field named foo will yield a getter and setter of that bit's content of the forms

const pub fn foo(&self) -> bool;

const pub fn set_foo(&mut self, value: bool) -> &mut Self;

Otherwise, a named field foo that does not specify a custom representation (given by $repr above) yields a getter and setter over its range

const pub fn foo(&self) -> MinWidth<$high, $low>;

const pub fn set_foo(&mut self, value: MinWidth<$high, $low>) -> &mut Self;

where MinWidth<$high, $low> is the smallest unsigned integral type of bit size at least $high - $low + 1.

Each getter and setter is annotated with an auto-generated doc string referencing the corresponding bit range (i.e., TypeName[hi:lo] or TypeName[bit]). Doc comments on field declarations are also forwarded to the getter, and appear before the auto-generated line.

A named field may be annotated with #[unshifted], which results in the getter and setter operating on values in their original bit position within the base type rather than shifting them down. For example, the setter and getter for a field at [19:16] would expect and return values like 0x50000 instead of 0x5. #[unshifted] is incompatible with reserved fields and fields with custom representations.

If an expression is given on the right-hand side of a field declaration, this indicates a default value in the case of a named field or a reserved-as value in the case of a reserved value. More on that below.

Note that a reserved field with no reserved-as value has no semantic meaning and is purely for documentation's sake.

Fields with custom representations

Fields (of width > 1) can be given more structure with a custom representation, which is a type specified as $repr above. The corresponding getters and setter are of the forms

fn foo(&self) -> $repr
where
    $repr: zerocopy::TryFromBytes;

fn try_foo(&self) -> Result<$repr, bitrs::InvalidBits<MinWidth<$high, $low>>>
where
    $repr: zerocopy::TryFromBytes;

fn set_foo(&mut self, value: $repr)
where
    $repr: zerocopy::IntoBytes + zerocopy::Immutable;

Not all bit patterns are necessarily valid with a custom representation. foo panics on an invalid pattern; try_foo returns an InvalidBits error wrapping the invalid pattern.

A custom representation must - definitionally - satisfy the three zerocopy traits above, which are leveraged for safe and efficient transmutation between MinWidth<$high, $low> and $repr. They are all derivable.

Another requirement of a custom representation is that it implements Debug, which is used in the formatting of the layout type.

For brevity and readability, the bitfield_repr attribute may be used to define a custom representation.

new() and default(); default and reserved-as values

Reserved-as field values reflect values that fields must have, modeling hardware requirements in the case of registers. Given that, new() will yield an otherwise-zeroed base value with the reserved-as values set.

Default field values reflect desired defaults, possibly modeling reset values in the case of registers. default() will yield an otherwise-zeroed base value with both the default and reserved-as values set.

Associated constants

In C, one would accomplish bitfield manipulation through manual masking and shifting, usually with an equivalent set of FOO_MASK and FOO_SHIFT preprocessor variables. Even though we have more structure at our disposal in this context, it can sometimes be convenient to have these raw masks and shifts on hand. One example is when building up a register value in inline assembly. Accordingly, the layout type will have the associated constants of FOO_MASK: $base and FOO_SHIFT: usize for each named field foo of width > 1; for a width-1 field named foo only a FOO_BIT: usize constant will be defined, representing the shift. Further, RSVD1_MASK: $base and RSVD0_MASK: $base are defined, giving the mask of reserved-as bits that should be set or unset, as well as DEFAULT: $base giving the default layout value.

Iteration

The layout type admits iterators over field values and metadata. An iterator can be accessed via iter(), and IntoIterator is implemented by the layout type and references to it. Its item type is (&'static bitrs::FieldMetadata<$base>, $base). See FieldMetadata for more info.

Iterators and iteration are both cheap, with the associated metadata being defined as a static constant.

Custom Field Representations

bitfield_repr is an attribute that is syntactic sugar for deriving repr(X) and the handful of traits expected of a custom field representation.

Why another crate for bitfields?

There are already a handful out there, so why this one too? It is the author‘s opinion that none of those at the time of writing this offer all of the above features (e.g., around reserved semantics) or the author’s desired ergonomics around register modeling. For example, some constrain field specification by bit width instead of by an explicit bit range, which is not how registers are commonly described in official references (plus, the author surely can't trust himself to do mental math like that).