Skip to main content

bnb/guide/
migrating.rs

1//! Coming from `binrw`, `modular-bitfield`, or `num_enum` — how the mental models map.
2//!
3//! `bnb` folds the jobs of several crates into one surface (see the crate's
4//! `ACKNOWLEDGMENTS.md`), and its
5//! attribute vocabulary deliberately echoes theirs — so if you already think in one of
6//! them, most of your instincts carry over. This page maps each crate's spellings to
7//! `bnb`'s and, more importantly, calls out where they genuinely differ.
8//!
9//! `bnb` examples here are runnable doctests; the other crates aren't dependencies, so
10//! their snippets are illustrative (shown `ignore`d or in prose).
11//!
12//! # From `binrw`
13//!
14//! [`binrw`](https://docs.rs/binrw) is the closest relative: `#[bin]`'s
15//! `#[br]`/`#[bw]`/`#[brw]` attribute split is modeled directly on it, and where a
16//! spelling is reused it means what binrw means. A binrw user is immediately at home.
17//!
18//! ## Directive mapping
19//!
20//! Most directives are spelled identically:
21//!
22//! | binrw | `bnb` | notes |
23//! |-------|-------|-------|
24//! | `#[br(..)]` / `#[bw(..)]` / `#[brw(..)]` | same | read / write / both |
25//! | `#[br(big)]` / `#[br(little)]` | `#[bin(big)]` / `#[bin(little)]` | byte order — see below |
26//! | `magic = b"..."` / `magic = 0u8` | struct: `magic = <Bits expr>`; enum dispatch: `magic = b"..."` | same idea; sub-byte magics allowed |
27//! | `count = <expr>` | `count = <expr>` | length-driven `Vec` |
28//! | `calc = <expr>` | `calc = <expr>` | computed field (pair with `temp`) |
29//! | `temp` | `temp` | read into a local, don't store |
30//! | `map = <f>` / `try_map = <f>` | `map = <f>` / `try_map = <f>` | transform the wire value |
31//! | `if(<cond>)` | `if(<cond>)` | conditional `Option` |
32//! | `parse_with = <f>` / `write_with = <f>` | `parse_with = <f>` / `write_with = <f>` | custom field codec |
33//! | `ignore` / `default` | `ignore` (as `#[brw(ignore)]`) | neither read nor written |
34//! | `assert(<cond>)` | `assert(<cond>)` (as `#[br(assert(..))]`) | decode-time guard; `bnb`'s is read-only |
35//! | `pad_before` / `pad_after` | `pad_before` / `pad_after` | in **bits**, not bytes |
36//! | `align_before` / `align_after` | `align_before` / `align_after` | to the next byte boundary |
37//! | `restore_position` | `restore_position` | peek without consuming |
38//! | `seek_before` | `seek` | jump to an absolute offset |
39//! | `dbg` | `dbg` (`std`-only; a `tracing` event) | trace a field as it decodes |
40//! | `import` / `args` | `ctx(..)` + field `ctx { .. }` | parse context; `bnb`'s is decode-only |
41//!
42//! So a binrw struct like this:
43//!
44//! ```ignore
45//! # // binrw — illustrative, not compiled here.
46//! #[binrw]
47//! #[brw(big, magic = b"BM")]
48//! struct Header {
49//!     #[br(temp)]
50//!     #[bw(calc = items.len() as u16)]
51//!     count: u16,
52//!     #[br(count = count)]
53//!     items: Vec<u16>,
54//! }
55//! ```
56//!
57//! becomes, in `bnb` — nearly the same, and the length-prefixed idiom collapses to one
58//! directive. (A struct-level `magic` takes a [`Bits`](crate::Bits) value — here the
59//! `u16` `0x424D` = `"BM"`; a *byte-string* magic like binrw's `b"BM"` is the
60//! [enum-dispatch](super::dispatch) spelling, verified by peeking the wire.)
61//!
62//! ```
63//! use bnb::bin;
64//!
65//! #[bin(big, magic = 0x424Du16)]   // "BM"
66//! #[derive(Debug, PartialEq)]
67//! struct Header {
68//!     #[brw(count_prefix = u16)]   // the temp+calc+count triad, in one line
69//!     items: Vec<u16>,
70//! }
71//!
72//! let h = Header { items: vec![0xAABB, 0xCCDD] };
73//! assert_eq!(h.to_bytes().unwrap(), [b'B', b'M', 0x00, 0x02, 0xAA, 0xBB, 0xCC, 0xDD]);
74//! assert_eq!(Header::decode_exact(&h.to_bytes().unwrap()).unwrap(), h);
75//! ```
76//!
77//! ## The real differences
78//!
79//! - **Bit offsets, not just byte offsets.** binrw is byte-oriented — it reads and
80//!   writes through `io::Read`/`io::Write` + `Seek`, so a field starts on a byte
81//!   boundary. `bnb` reads and writes at arbitrary **bit** offsets: a `u4`, a `u12`, a
82//!   3-bit flag run, or a whole `#[bitfield]` sits mid-byte with no manual shifting. This
83//!   is the capability that separates the two, and it is why `pad_before`/`seek` amounts
84//!   are counted in **bits** (via `n.bits()` / `n.bytes()` from the
85//!   [`prelude`](crate::prelude)), not bytes.
86//! - **Byte order is spelled `bytes` (and there's a `bits` knob too).** binrw sets
87//!   endianness with `big`/`little`. `bnb` uses the same `big`/`little` keywords —
88//!   `#[bin(big)]` is sugar — but the full spelling is `#[bin(bytes = big|little)]`, and
89//!   there is a *second*, independent knob binrw has no equivalent for: `bits = msb|lsb`,
90//!   the bit order within a byte. The two compose by the natural-layout rule (see
91//!   [`bin_codec`](super::bin_codec)).
92//! - **`no_std` + `alloc`.** Both are `no_std`; `bnb` additionally ships a push/pull
93//!   framing buffer ([`BitBuf`](crate::BitBuf), with a bounded/alloc-once mode) for
94//!   embedded transports, plus an [I/O ladder](super::io) that scales from `&[u8]` up to
95//!   `std` streams, `bytes`, and `tokio` framing.
96//! - **A dual-use encode model.** [`to_bytes`](crate::BitEncode) is *verbatim* —
97//!   byte-identical to what you decoded, retained reserved bits and all — while
98//!   `to_canonical_bytes` normalizes (`reserved` → spec, `calc` recomputed). You pick per
99//!   call. binrw always recomputes on write; `bnb` lets you observe and re-emit a peer's
100//!   exact bytes, which matters for a security tool. See [`dual_use`](super::dual_use).
101//! - **More than a codec.** binrw is read/write only. `bnb` also gives you
102//!   [`#[bitfield]`](super::bitfields), [`#[bitflags]`](super::flags), and
103//!   [`BitEnum`](super::enums) as first-class field types (below), plus
104//!   [`WireLen`](crate::WireLen)/`auto_len` for auto-deriving, overridable length fields.
105//! - **Whole-struct wire mapping.** binrw's `map`/`try_map` transform a *field*; `bnb`
106//!   adds the same at the struct level (`#[bin(wire = W)]` / `#[bin(map = .., bw_map = ..)]`),
107//!   a whole logical type serialized via a separate wire type. See [`mapping`](super::mapping).
108//!
109//! A few binrw directives have no direct `bnb` equivalent (or a different home):
110//! `import_raw`/`args_raw`, `offset` (binrw's `FilePtr`), `try_calc`, `pre_assert`, and
111//! `pad_size_to`. Reach for `parse_with`/`write_with` or `seek` + `restore_position` for
112//! those shapes.
113//!
114//! # From `modular-bitfield`
115//!
116//! [`modular-bitfield`](https://docs.rs/modular-bitfield) (and the similar
117//! [`bitfield-struct`](https://docs.rs/bitfield-struct)) packs sub-byte fields into a
118//! byte array. `bnb`'s [`#[bitfield]`](super::bitfields) does the same job, backed by a
119//! single unsigned integer, and — crucially — the result is a [`Bits`](crate::Bits)
120//! value that drops straight into a `#[bin]` message.
121//!
122//! ## Field types and widths
123//!
124//! modular-bitfield uses opaque specifier types `B1`, `B2`, … `B128`, with an optional
125//! `#[bits = N]` compile-time check. `bnb` uses the **real arbitrary-width integers**
126//! `u1`..`u127` as the field types, so a field's width is usually *inferred* from its
127//! type — no separate specifier:
128//!
129//! | modular-bitfield | `bnb` |
130//! |------------------|-------|
131//! | `a: B5` | `a: u5` (width inferred) |
132//! | `a: B1` | `a: bool` or `a: u1` |
133//! | `#[bits = 5] a: SomeType` | `#[bits(5)] a: SomeType` (note: parens, not `=`) |
134//! | `#[skip] __: B3` | a `#[bits(A..=B)]` gap, or just don't name those bits |
135//! | struct-level `bits = 32` guard | the backing integer, e.g. `#[bitfield(u32, ..)]` |
136//!
137//! ```ignore
138//! # // modular-bitfield — illustrative.
139//! #[bitfield]
140//! struct VersionIhl { version: B4, ihl: B4 }
141//! ```
142//!
143//! ```
144//! use bnb::{bitfield, u4};
145//!
146//! #[bitfield(u8, bits = msb, bytes = big)]
147//! #[derive(Clone, Copy)]
148//! struct VersionIhl { version: u4, ihl: u4 }
149//!
150//! let v = VersionIhl::new().with_version(u4::new(4)).with_ihl(u4::new(5));
151//! assert_eq!(v.to_raw(), 0x45);
152//! ```
153//!
154//! ## Accessors
155//!
156//! The accessor shapes line up closely:
157//!
158//! | modular-bitfield | `bnb` |
159//! |------------------|-------|
160//! | `fieldname()` (getter, panics on invalid) | `fieldname()` (returns the typed value) |
161//! | `set_fieldname(v)` | `set_fieldname(v)` |
162//! | `with_fieldname(v)` | `with_fieldname(v)` (chainable) |
163//! | `set_fieldname_checked` / `with_..._checked` | field types are range-checked at construction |
164//! | `new()` | `new()` (all-zero) |
165//! | `from_bytes([u8; N])` / `into_bytes()` | `from_bytes(..)` / `to_bytes()` (declared `bytes` order) |
166//! | — | `to_raw()` / `from_raw()` (the backing integer directly) |
167//! | — | `to_be_bytes` / `to_le_bytes` (endianness override) |
168//!
169//! ## What `bnb` adds
170//!
171//! - **Explicit bit *and* byte order.** `#[bitfield(u16, bits = msb, bytes = big)]` — the
172//!   first-declared field lands in the high (`msb`) or low (`lsb`) bits, and `to_bytes`
173//!   serializes in the declared byte order. modular-bitfield fixes an LSB-first layout.
174//! - **It nests into `#[bin]`.** A `bnb` bitfield is a `Bits` value, so it drops into a
175//!   whole-message [`#[bin]`](super::bin_codec) struct as a single field, and other
176//!   bitfields / `BitEnum`s nest *inside* it — all width-checked at compile time.
177//! - **A custom `Debug`.** `#[derive(Debug)]` on a `bnb` bitfield decomposes the *logical*
178//!   fields (`version: 4, ihl: 5`) rather than printing the opaque backing integer.
179//!
180//! ```
181//! use bnb::{bin, bitfield, u4};
182//!
183//! #[bitfield(u8, bits = msb, bytes = big)]
184//! #[derive(Clone, Copy, Debug)]
185//! struct VersionIhl { version: u4, ihl: u4 }
186//!
187//! // The bitfield is just another field of a whole-message struct.
188//! #[bin(big)]
189//! #[derive(Debug)]
190//! struct Ipv4Start { vi: VersionIhl, tos: u8, total_len: u16 }
191//!
192//! let h = Ipv4Start {
193//!     vi: VersionIhl::new().with_version(u4::new(4)).with_ihl(u4::new(5)),
194//!     tos: 0,
195//!     total_len: 40,
196//! };
197//! assert_eq!(h.to_bytes().unwrap(), [0x45, 0x00, 0x00, 0x28]);
198//! ```
199//!
200//! # From `num_enum`
201//!
202//! [`num_enum`](https://docs.rs/num_enum) maps a `#[repr(uN)]` enum to and from its
203//! primitive discriminant. `bnb`'s [`#[derive(BitEnum)]`](super::enums) does the same and
204//! goes sub-byte: a `bnb` enum can be 3 or 12 bits wide, and it nests into a
205//! `#[bitfield]` or a `#[bin]` message.
206//!
207//! ## Derive and attribute mapping
208//!
209//! | num_enum | `bnb` |
210//! |----------|-------|
211//! | `#[derive(TryFromPrimitive, IntoPrimitive)]` | `#[derive(BitEnum)]` |
212//! | `#[repr(u8)]` (required) | `#[bit_enum(u8)]` + `#[repr(u8)]` |
213//! | `#[derive(FromPrimitive)]` + `#[num_enum(default)]` | `#[catch_all]` (a total, lossless `From`) |
214//! | `#[num_enum(catch_all)] Other(u8)` | `#[catch_all] Other(u8)` (tuple variant holds the width) |
215//! | closed set (no default) → `TryFromPrimitive` | `closed` → checked `TryFrom` |
216//! | `TryFromPrimitiveError` | [`UnknownDiscriminant`](crate::UnknownDiscriminant) |
217//! | `#[num_enum(alternatives = [..])]` | (fold into `#[catch_all]`, or match on the raw value) |
218//!
219//! num_enum's `catch_all` variant must be a tuple variant holding the exact repr type;
220//! `bnb`'s is the same, holding the width type (which *is* the repr type for a primitive
221//! width). A num_enum enum:
222//!
223//! ```ignore
224//! # // num_enum — illustrative.
225//! #[derive(TryFromPrimitive, IntoPrimitive)]
226//! #[repr(u8)]
227//! enum EtherType { Ipv4 = 0x00, Arp = 0x06 }
228//! ```
229//!
230//! becomes, with a catch-all so decoding is total and lossless (the dual-use default):
231//!
232//! ```
233//! #[derive(bnb::BitEnum, Clone, Copy, Debug, PartialEq, Eq)]
234//! #[bit_enum(u8)]
235//! #[repr(u8)]
236//! enum EtherType { Ipv4 = 0x00, Arp = 0x06, #[catch_all] Other(u8) }
237//!
238//! // The num_enum-parity primitive conversions come for free at primitive widths:
239//! assert_eq!(u8::from(EtherType::Arp), 0x06);              // IntoPrimitive
240//! assert_eq!(EtherType::from(0x06u8), EtherType::Arp);     // total (has catch-all)
241//! assert_eq!(EtherType::from(0x99u8), EtherType::Other(0x99));
242//! ```
243//!
244//! For a set you assert is closed, mark it `closed` and you get the checked `TryFrom`,
245//! erroring with [`UnknownDiscriminant`](crate::UnknownDiscriminant) — num_enum's
246//! `TryFromPrimitive`/`TryFromPrimitiveError` role:
247//!
248//! ```
249//! #[derive(bnb::BitEnum, Clone, Copy, Debug, PartialEq, Eq)]
250//! #[bit_enum(u8, closed)]
251//! #[repr(u8)]
252//! enum Direction { Request = 1, Reply = 2 }
253//!
254//! assert_eq!(Direction::try_from(2u8), Ok(Direction::Reply));
255//! assert!(Direction::try_from(7u8).is_err());   // UnknownDiscriminant
256//! ```
257//!
258//! ## What `bnb` adds
259//!
260//! - **Bit-native reprs.** num_enum requires a primitive `#[repr]`. `bnb` enums can be
261//!   *sub-byte* (`#[bit_enum(u3)]`) or byte-aligned-but-non-primitive (`#[bit_enum(u24)]`).
262//!   At those widths the enum gets the [`Bits`](crate::Bits) impls but not the primitive
263//!   `From`/`TryFrom` (there's no `u3` primitive to convert with) — it's meaningful only
264//!   nested where its bits are placed in context.
265//! - **It nests.** A `BitEnum` drops into a [`#[bitfield]`](super::bitfields) or a
266//!   [`#[bin]`](super::bin_codec) message as one field, contributing exactly its declared
267//!   bits.
268//! - **The `num_enum` parity is automatic at primitive widths.** A `u8`/`u16`/`u32`/`u64`/
269//!   `u128`-width enum emits the same `From`/`TryFrom<primitive>` you'd reach num_enum for
270//!   — no separate derive, no hand-written round-trip test. See [`enums`](super::enums).
271//!
272//! # Where to go next
273//!
274//! - [`quick_start`](super::quick_start) — the five-minute tour if you skipped it.
275//! - [`bin_codec`](super::bin_codec) / [`directives`](super::directives) — the `#[bin]`
276//!   surface in full (the binrw analogue).
277//! - [`bitfields`](super::bitfields) / [`enums`](super::enums) / [`flags`](super::flags) —
278//!   the field types (the modular-bitfield / num_enum analogues).
279//! - [`composition`](super::composition) — how all of the above nest and size each other.