Expand description
The I/O ladder — where the codec reads from and writes to.
The everyday entry points (decode_exact/decode_all/peek/to_bytes) work on byte
slices and Vecs. When you need to read from a socket or a file, decode takes any
Source cursor; to write into an explicit Sink,
BitEncode::bit_encode does the dual.
Pick the source by what your input can do:
| Source | Backing | Can seek? | Use for |
|---|---|---|---|
BitReader | &[u8] slice | yes (free cursor math) | in-memory bytes |
StreamBitReader | any Read | no (forward only) | a stream you read once |
BufSource | any Read | yes (within a bounded buffer) | a socket that also needs to seek |
BitBuf | owned Vec<u8> (pushable) | yes (cursor math) | incremental framing: push bytes, pull messages |
SeekReader | Read + Seek | yes (via io::Seek) | a large file / container |
BytesReader (bytes feature) | owned Bytes | yes | zero-copy async framing |
Seeking is only needed by messages that use #[br(restore_position)]; everything
else runs over the forward-only StreamBitReader too.
decode reads in the source’s layout, not the message’s. A Source
carries its own byte/bit order, and Type::decode(&mut source) reads in that order.
The plain constructors (StreamBitReader::new,
BufSource::new, SeekReader::new)
default to msb/big — correct for a default-layout message, but a non-default
(little/lsb) message decoded through them is silently misread. Build the source
with the message’s layout: StreamBitReader::with_layout(r, <Msg as bnb::BitEncode>::LAYOUT)
(likewise SeekReader::with_layout and BufSource::with_capacity_and_layout). The slice
entry points (decode_exact/decode_all/peek) sidestep this — they bake Msg’s layout
in — so they are the foolproof “decode this buffer” path.
§The low-level cursor
BitReader/BitWriter are the bit cursors
under everything — read or write any Bits value at the current bit
offset:
use bnb::{BitReader, BitWriter, u4, u12};
let mut w = BitWriter::new();
w.write(u4::new(0xA)).unwrap();
w.write(u12::new(0xBCD)).unwrap();
assert_eq!(w.into_bytes(), [0xAB, 0xCD]);
let mut r = BitReader::new(&[0xAB, 0xCD]);
assert_eq!(r.read::<u4>().unwrap(), u4::new(0xA));
assert_eq!(r.read::<u12>().unwrap(), u12::new(0xBCD));§decode over each source
The same message decodes from a slice cursor, a forward stream, a buffered socket, or a seekable file — only the source type changes:
use bnb::{bin, BitReader, StreamBitReader, BufSource, SeekReader};
use std::io::Cursor;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Word { value: u32 }
let bytes = [0x12, 0x34, 0x56, 0x78];
// in-memory slice cursor
let mut r = BitReader::new(&bytes);
assert_eq!(Word::decode(&mut r).unwrap(), Word { value: 0x1234_5678 });
// a forward-only `Read` (a `&[u8]` is `Read` but not `Seek`)
let mut s = StreamBitReader::new(&bytes[..]);
assert_eq!(Word::decode(&mut s).unwrap(), Word { value: 0x1234_5678 });
// a `Read` with a bounded retain-and-seek buffer (the socket case)
let mut b = BufSource::new(&bytes[..]);
assert_eq!(Word::decode(&mut b).unwrap(), Word { value: 0x1234_5678 });
// a `Read + Seek` (a file; here a Cursor over a Vec)
let mut f = SeekReader::new(Cursor::new(bytes.to_vec()));
assert_eq!(Word::decode(&mut f).unwrap(), Word { value: 0x1234_5678 });§Encoding
to_bytes() (the common case) returns a Vec; encode(&mut impl Write) writes straight
to a socket or file, and bit_encode(&mut impl Sink) targets
an explicit bit sink (for composing into a cursor you already hold). encode is always
verbatim; for the canonical form encode value.to_canonical() — see
Two encode forms.
use bnb::bin;
use bnb::EncodeExt; // brings `.encode(&mut impl Write)` into scope (the `std` feature)
let w = Word { value: 0x1234_5678 };
assert_eq!(w.to_bytes().unwrap(), [0x12, 0x34, 0x56, 0x78]);
let mut out: Vec<u8> = Vec::new(); // any std::io::Write
w.encode(&mut out).unwrap(); // Word has no canonical form → always verbatim
assert_eq!(out, [0x12, 0x34, 0x56, 0x78]);§The bytes feature
With --features bytes, BytesReader/BytesWriter decode from / encode to the
bytes crate’s Bytes/BytesMut for zero-copy async framing:
// requires `bnb = { features = ["bytes"] }`
use bnb::{BytesReader, BytesWriter, Sink};
let mut w = BytesWriter::new();
w.write(0x1234u16).unwrap();
let frame = w.freeze(); // a zero-copy `bytes::Bytes`
let mut r = BytesReader::new(frame); // owns the frame, no copy§Bridging to std::io
The ladder above adapts a std::io::Read into a Source
(BufSource/SeekReader). The reverse —
handing a bnb cursor to std::io-based code from a parse_with/write_with — is
Source::as_read and Sink::as_write,
byte views over the cursor. With From<io::Error>, std::io results ? straight
into a BitError:
use bnb::{BitError, BitReader, Source};
use std::io::Read;
fn read_three<S: Source>(r: &mut S) -> Result<[u8; 3], BitError> {
let mut buf = [0u8; 3];
r.as_read().read_exact(&mut buf)?; // a `std::io::Read` view over the cursor
Ok(buf)
}
let mut r = BitReader::new(&[0xAA, 0xBB, 0xCC]);
assert_eq!(read_three(&mut r).unwrap(), [0xAA, 0xBB, 0xCC]);§Streaming and partial input
A StreamBitReader or BufSource
that runs out mid-message reports ErrorKind::Incomplete, the
“read more bytes and retry” signal — distinct from a definitive parse failure. See
errors.
When bytes arrive in pieces from something that isn’t a Read (a channel, a callback, an
async chunk), BitBuf is the push/pull counterpart: push(&bytes) as
they come, pull::<T>() to take whole messages off the front (it returns None until a full
message is buffered). BitBuf is itself a SeekSource, so it also reads
through plain decode (Type::decode(&mut bitbuf)); pull adds the
reclaim + layout-baking + None-on-incomplete on top. Reclaim is deferred and in place, so a
push/pull loop reuses one allocation; for a guaranteed-fixed footprint use
BitBuf::bounded(cap) with try_push
(which refuses to grow) and grow for explicit resizing.
§Reading and writing one connection (without try_clone)
To run a request/response loop on a single TCP connection you need to read and write the
same socket. You don’t need try_clone() (which dups the fd): std’s &TcpStream
implements both Read and Write, so wrap the read
half in a BufSource and write through &TcpStream — two shared borrows
of the same socket:
use bnb::{bin, BufSource};
use std::io::Write;
use std::net::TcpStream;
let stream = TcpStream::connect("127.0.0.1:9000")?;
let mut reader = BufSource::new(&stream); // &TcpStream: Read
let mut writer = &stream; // &TcpStream: Write — the same socket
writer.write_all(&Msg { seq: 1 }.to_bytes()?)?;
let reply = Msg::decode(&mut reader)?; // one framed message off the streamFor halves you need to move across threads (a dedicated reader thread and writer thread),
the equivalent of tokio’s into_split is Arc<TcpStream> — clone the Arc per side and use
&*arc (still Read + Write), no try_clone. The runnable examples/tcp.rs shows a full
client/server.
For an ergonomic wrapper, the net feature adds MessageStream — it owns a Read + Write stream and exposes read_message/write_message (so you exchange #[bin] values,
not bytes) — and MessageDatagram, the datagram counterpart over a sealed DatagramSocket
(UdpSocket or UnixDatagram) with send_message/recv_message; both are unit-testable
without a real socket via the mock feature. With tokio, BinCodec does the same
for an async Framed stream.