Skip to main content

bnb/guide/
composition.rs

1//! Composition — how the pieces nest and size each other.
2//!
3//! Everything in `bnb` is built on one idea: a value that occupies a fixed number of
4//! bits ([`Bits`](crate::Bits)). Because that is the unit of composition, the macros
5//! stack without any glue — a sub-byte enum nests in a packed word, which nests in a
6//! byte-aligned message — and the widths are checked by the compiler, not guessed by
7//! the macro.
8//!
9//! # The layers
10//!
11//! ```text
12//!   #[bin] message            ← fields read/written at arbitrary bit offsets
13//!     └─ #[bitfield] word      ← several Bits values packed into one integer
14//!          ├─ #[derive(BitEnum)]   ← an integer discriminant (a Bits value)
15//!          └─ #[bitflags] set      ← single-bit flags (a Bits value)
16//!     └─ u1..u127 / bool / u8..    ← the leaf Bits values
17//! ```
18//!
19//! Each arrow is "is a `Bits` value", so each layer is just a field of the one above.
20//!
21//! # A worked stack
22//!
23//! A 5-bit enum and a 3-bit flag set packed into one byte, with that byte a field of a
24//! larger message — three layers, checked to fit at compile time:
25//!
26//! ```
27//! use bnb::{bin, bitfield, bitflags, BitEnum, u5};
28//!
29//! #[derive(BitEnum, Clone, Copy, Debug, PartialEq, Eq)]
30//! #[bit_enum(u5)]
31//! enum Kind { Hello, Data, Bye, #[catch_all] Other(u5) }
32//!
33//! #[bitflags(u8)]
34//! #[derive(Clone, Copy, Debug, PartialEq, Eq)]
35//! struct Flags { urgent: bool, signed: bool, compressed: bool }
36//!
37//! // 5-bit Kind + a 3-bit slice of Flags would overflow a u8 together with Kind,
38//! // so pack Kind (5) with three explicit bools (3) — exactly 8 bits.
39//! #[bitfield(u8, bits = msb)]
40//! #[derive(Clone, Copy, Debug, PartialEq, Eq)]
41//! struct Tag { kind: Kind, urgent: bool, signed: bool, compressed: bool }
42//!
43//! #[bin(big)]
44//! #[derive(Debug, PartialEq)]
45//! struct Frame { tag: Tag, length: u16 }   // Tag (1 byte) + length (2 bytes)
46//!
47//! let frame = Frame {
48//!     tag: Tag::new().with_kind(Kind::Data).with_urgent(true),
49//!     length: 512,
50//! };
51//! let bytes = frame.to_bytes().unwrap();
52//! assert_eq!(bytes.len(), 3);
53//! assert_eq!(Frame::decode_exact(&bytes).unwrap(), frame);
54//! ```
55//!
56//! # Widths are compiler-checked, never guessed
57//!
58//! A `#[bitfield]` can't know the bit width of a field whose type is another bitfield
59//! or enum — that lives in `<T as Bits>::BITS`. So instead of computing offsets, the
60//! macro emits **const expressions** (`<T as Bits>::BITS`, cumulative sums, masks) that
61//! the compiler evaluates. Invalid layouts are rejected at compile time — for example
62//! a reversed manual range is a clear error, not a silent miscompile:
63//!
64//! ```compile_fail
65//! use bnb::bitfield;
66//! #[bitfield(u16, bits = msb)]
67//! #[derive(Clone, Copy)]
68//! struct Bad { #[bits(15..=0)] x: bnb::u16 } // reversed range (write it low..=high)
69//! # let _ = Bad::new();
70//! ```
71//!
72//! # Fixed vs. variable length: `FixedBitLen`
73//!
74//! A message with no variable-length field has a compile-time-constant encoded width
75//! and implements [`FixedBitLen`](crate::FixedBitLen). That is what lets a fixed
76//! message be embedded as a sized region inside another. A `count`-driven `Vec` (or a
77//! `ctx`/`if`/positioning field) makes a message variable-length, so it implements the
78//! codec traits but **not** `FixedBitLen`:
79//!
80//! ```
81//! use bnb::{bin, FixedBitLen};
82//!
83//! #[bin(big)]
84//! #[derive(Debug)]
85//! struct Fixed { a: u16, b: u8 }       // always 24 bits
86//!
87//! assert_eq!(<Fixed as FixedBitLen>::BIT_LEN, 24);
88//! ```
89//!
90//! See [`numbers`](super::numbers) for the `Bits` contract at the bottom of the stack,
91//! and [`bitfields`](super::bitfields)/[`bin_codec`](super::bin_codec) for the layers
92//! above it.