Skip to main content

Module migrating

Module migrating 

Source
Expand description

Coming from binrw, modular-bitfield, or num_enum — how the mental models map.

bnb folds the jobs of several crates into one surface (see the crate’s ACKNOWLEDGMENTS.md), and its attribute vocabulary deliberately echoes theirs — so if you already think in one of them, most of your instincts carry over. This page maps each crate’s spellings to bnb’s and, more importantly, calls out where they genuinely differ.

bnb examples here are runnable doctests; the other crates aren’t dependencies, so their snippets are illustrative (shown ignored or in prose).

§From binrw

binrw is the closest relative: #[bin]’s #[br]/#[bw]/#[brw] attribute split is modeled directly on it, and where a spelling is reused it means what binrw means. A binrw user is immediately at home.

§Directive mapping

Most directives are spelled identically:

binrwbnbnotes
#[br(..)] / #[bw(..)] / #[brw(..)]sameread / write / both
#[br(big)] / #[br(little)]#[bin(big)] / #[bin(little)]byte order — see below
magic = b"..." / magic = 0u8struct: magic = <Bits expr>; enum dispatch: magic = b"..."same idea; sub-byte magics allowed
count = <expr>count = <expr>length-driven Vec
calc = <expr>calc = <expr>computed field (pair with temp)
temptempread into a local, don’t store
map = <f> / try_map = <f>map = <f> / try_map = <f>transform the wire value
if(<cond>)if(<cond>)conditional Option
parse_with = <f> / write_with = <f>parse_with = <f> / write_with = <f>custom field codec
ignore / defaultignore (as #[brw(ignore)])neither read nor written
assert(<cond>)assert(<cond>) (as #[br(assert(..))])decode-time guard; bnb’s is read-only
pad_before / pad_afterpad_before / pad_afterin bits, not bytes
align_before / align_afteralign_before / align_afterto the next byte boundary
restore_positionrestore_positionpeek without consuming
seek_beforeseekjump to an absolute offset
dbgdbg (std-only; a tracing event)trace a field as it decodes
import / argsctx(..) + field ctx { .. }parse context; bnb’s is decode-only

So a binrw struct like this:

#[binrw]
#[brw(big, magic = b"BM")]
struct Header {
    #[br(temp)]
    #[bw(calc = items.len() as u16)]
    count: u16,
    #[br(count = count)]
    items: Vec<u16>,
}

becomes, in bnb — nearly the same, and the length-prefixed idiom collapses to one directive. (A struct-level magic takes a Bits value — here the u16 0x424D = "BM"; a byte-string magic like binrw’s b"BM" is the enum-dispatch spelling, verified by peeking the wire.)

use bnb::bin;

#[bin(big, magic = 0x424Du16)]   // "BM"
#[derive(Debug, PartialEq)]
struct Header {
    #[brw(count_prefix = u16)]   // the temp+calc+count triad, in one line
    items: Vec<u16>,
}

let h = Header { items: vec![0xAABB, 0xCCDD] };
assert_eq!(h.to_bytes().unwrap(), [b'B', b'M', 0x00, 0x02, 0xAA, 0xBB, 0xCC, 0xDD]);
assert_eq!(Header::decode_exact(&h.to_bytes().unwrap()).unwrap(), h);

§The real differences

  • Bit offsets, not just byte offsets. binrw is byte-oriented — it reads and writes through io::Read/io::Write + Seek, so a field starts on a byte boundary. bnb reads and writes at arbitrary bit offsets: a u4, a u12, a 3-bit flag run, or a whole #[bitfield] sits mid-byte with no manual shifting. This is the capability that separates the two, and it is why pad_before/seek amounts are counted in bits (via n.bits() / n.bytes() from the prelude), not bytes.
  • Byte order is spelled bytes (and there’s a bits knob too). binrw sets endianness with big/little. bnb uses the same big/little keywords — #[bin(big)] is sugar — but the full spelling is #[bin(bytes = big|little)], and there is a second, independent knob binrw has no equivalent for: bits = msb|lsb, the bit order within a byte. The two compose by the natural-layout rule (see bin_codec).
  • no_std + alloc. Both are no_std; bnb additionally ships a push/pull framing buffer (BitBuf, with a bounded/alloc-once mode) for embedded transports, plus an I/O ladder that scales from &[u8] up to std streams, bytes, and tokio framing.
  • A dual-use encode model. to_bytes is verbatim — byte-identical to what you decoded, retained reserved bits and all — while to_canonical_bytes normalizes (reserved → spec, calc recomputed). You pick per call. binrw always recomputes on write; bnb lets you observe and re-emit a peer’s exact bytes, which matters for a security tool. See dual_use.
  • More than a codec. binrw is read/write only. bnb also gives you #[bitfield], #[bitflags], and BitEnum as first-class field types (below), plus WireLen/auto_len for auto-deriving, overridable length fields.
  • Whole-struct wire mapping. binrw’s map/try_map transform a field; bnb adds the same at the struct level (#[bin(wire = W)] / #[bin(map = .., bw_map = ..)]), a whole logical type serialized via a separate wire type. See mapping.

A few binrw directives have no direct bnb equivalent (or a different home): import_raw/args_raw, offset (binrw’s FilePtr), try_calc, pre_assert, and pad_size_to. Reach for parse_with/write_with or seek + restore_position for those shapes.

§From modular-bitfield

modular-bitfield (and the similar bitfield-struct) packs sub-byte fields into a byte array. bnb’s #[bitfield] does the same job, backed by a single unsigned integer, and — crucially — the result is a Bits value that drops straight into a #[bin] message.

§Field types and widths

modular-bitfield uses opaque specifier types B1, B2, … B128, with an optional #[bits = N] compile-time check. bnb uses the real arbitrary-width integers u1..u127 as the field types, so a field’s width is usually inferred from its type — no separate specifier:

modular-bitfieldbnb
a: B5a: u5 (width inferred)
a: B1a: bool or a: u1
#[bits = 5] a: SomeType#[bits(5)] a: SomeType (note: parens, not =)
#[skip] __: B3a #[bits(A..=B)] gap, or just don’t name those bits
struct-level bits = 32 guardthe backing integer, e.g. #[bitfield(u32, ..)]
#[bitfield]
struct VersionIhl { version: B4, ihl: B4 }
use bnb::{bitfield, u4};

#[bitfield(u8, bits = msb, bytes = big)]
#[derive(Clone, Copy)]
struct VersionIhl { version: u4, ihl: u4 }

let v = VersionIhl::new().with_version(u4::new(4)).with_ihl(u4::new(5));
assert_eq!(v.to_raw(), 0x45);

§Accessors

The accessor shapes line up closely:

modular-bitfieldbnb
fieldname() (getter, panics on invalid)fieldname() (returns the typed value)
set_fieldname(v)set_fieldname(v)
with_fieldname(v)with_fieldname(v) (chainable)
set_fieldname_checked / with_..._checkedfield types are range-checked at construction
new()new() (all-zero)
from_bytes([u8; N]) / into_bytes()from_bytes(..) / to_bytes() (declared bytes order)
to_raw() / from_raw() (the backing integer directly)
to_be_bytes / to_le_bytes (endianness override)

§What bnb adds

  • Explicit bit and byte order. #[bitfield(u16, bits = msb, bytes = big)] — the first-declared field lands in the high (msb) or low (lsb) bits, and to_bytes serializes in the declared byte order. modular-bitfield fixes an LSB-first layout.
  • It nests into #[bin]. A bnb bitfield is a Bits value, so it drops into a whole-message #[bin] struct as a single field, and other bitfields / BitEnums nest inside it — all width-checked at compile time.
  • A custom Debug. #[derive(Debug)] on a bnb bitfield decomposes the logical fields (version: 4, ihl: 5) rather than printing the opaque backing integer.
use bnb::{bin, bitfield, u4};

#[bitfield(u8, bits = msb, bytes = big)]
#[derive(Clone, Copy, Debug)]
struct VersionIhl { version: u4, ihl: u4 }

// The bitfield is just another field of a whole-message struct.
#[bin(big)]
#[derive(Debug)]
struct Ipv4Start { vi: VersionIhl, tos: u8, total_len: u16 }

let h = Ipv4Start {
    vi: VersionIhl::new().with_version(u4::new(4)).with_ihl(u4::new(5)),
    tos: 0,
    total_len: 40,
};
assert_eq!(h.to_bytes().unwrap(), [0x45, 0x00, 0x00, 0x28]);

§From num_enum

num_enum maps a #[repr(uN)] enum to and from its primitive discriminant. bnb’s #[derive(BitEnum)] does the same and goes sub-byte: a bnb enum can be 3 or 12 bits wide, and it nests into a #[bitfield] or a #[bin] message.

§Derive and attribute mapping

num_enumbnb
#[derive(TryFromPrimitive, IntoPrimitive)]#[derive(BitEnum)]
#[repr(u8)] (required)#[bit_enum(u8)] + #[repr(u8)]
#[derive(FromPrimitive)] + #[num_enum(default)]#[catch_all] (a total, lossless From)
#[num_enum(catch_all)] Other(u8)#[catch_all] Other(u8) (tuple variant holds the width)
closed set (no default) → TryFromPrimitiveclosed → checked TryFrom
TryFromPrimitiveErrorUnknownDiscriminant
#[num_enum(alternatives = [..])](fold into #[catch_all], or match on the raw value)

num_enum’s catch_all variant must be a tuple variant holding the exact repr type; bnb’s is the same, holding the width type (which is the repr type for a primitive width). A num_enum enum:

#[derive(TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
enum EtherType { Ipv4 = 0x00, Arp = 0x06 }

becomes, with a catch-all so decoding is total and lossless (the dual-use default):

#[derive(bnb::BitEnum, Clone, Copy, Debug, PartialEq, Eq)]
#[bit_enum(u8)]
#[repr(u8)]
enum EtherType { Ipv4 = 0x00, Arp = 0x06, #[catch_all] Other(u8) }

// The num_enum-parity primitive conversions come for free at primitive widths:
assert_eq!(u8::from(EtherType::Arp), 0x06);              // IntoPrimitive
assert_eq!(EtherType::from(0x06u8), EtherType::Arp);     // total (has catch-all)
assert_eq!(EtherType::from(0x99u8), EtherType::Other(0x99));

For a set you assert is closed, mark it closed and you get the checked TryFrom, erroring with UnknownDiscriminant — num_enum’s TryFromPrimitive/TryFromPrimitiveError role:

#[derive(bnb::BitEnum, Clone, Copy, Debug, PartialEq, Eq)]
#[bit_enum(u8, closed)]
#[repr(u8)]
enum Direction { Request = 1, Reply = 2 }

assert_eq!(Direction::try_from(2u8), Ok(Direction::Reply));
assert!(Direction::try_from(7u8).is_err());   // UnknownDiscriminant

§What bnb adds

  • Bit-native reprs. num_enum requires a primitive #[repr]. bnb enums can be sub-byte (#[bit_enum(u3)]) or byte-aligned-but-non-primitive (#[bit_enum(u24)]). At those widths the enum gets the Bits impls but not the primitive From/TryFrom (there’s no u3 primitive to convert with) — it’s meaningful only nested where its bits are placed in context.
  • It nests. A BitEnum drops into a #[bitfield] or a #[bin] message as one field, contributing exactly its declared bits.
  • The num_enum parity is automatic at primitive widths. A u8/u16/u32/u64/ u128-width enum emits the same From/TryFrom<primitive> you’d reach num_enum for — no separate derive, no hand-written round-trip test. See enums.

§Where to go next