Skip to main content

Module io

Module io 

Source
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:

SourceBackingCan seek?Use for
BitReader&[u8] sliceyes (free cursor math)in-memory bytes
StreamBitReaderany Readno (forward only)a stream you read once
BufSourceany Readyes (within a bounded buffer)a socket that also needs to seek
BitBufowned Vec<u8> (pushable)yes (cursor math)incremental framing: push bytes, pull messages
SeekReaderRead + Seekyes (via io::Seek)a large file / container
BytesReader (bytes feature)owned Bytesyeszero-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 stream

For 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.