Why wire-codec?
Most binary protocols boil down to three jobs: cut a byte stream into frames,
turn integers into compact wire representations, and pack tagged fields into
records. The Rust ecosystem has good crates for each of those jobs in
isolation, but they tend to assume a runtime (tokio, async-std), or a
specific serialization model (serde, prost), or a buffer abstraction
(bytes::Bytes, Vec<u8>) that propagates outward through your code.
wire-codec does not. It works on borrowed byte slices: you bring the buffer,
it provides the cursors, the codecs, and the framers. Nothing allocates on
the encode or decode path. Nothing pulls in an async runtime. Nothing forces
a serde-style derive. You can drop it into an embedded firmware build, a
WebAssembly module, a kernel-bypass network stack, or a synchronous CLI tool,
and the public API is exactly the same.
If you are building a network protocol crate, a binary serialization layer, or a framing adapter for some existing transport, this is intended to sit underneath that work.
Features
- Zero-copy I/O. [
ReadBuf] and [WriteBuf] are cursors over borrowed byte slices. Reads return sub-slices that borrow from the input; writes go straight into the caller's buffer. - Codec trait pair. [
Encode] and [Decode<'de>] are minimal, allocation-free, and composable. The'delifetime keeps borrowed payloads borrowed all the way through decoding. - LEB128 varint. Unsigned varint for
u32andu64with explicit overflow detection, compatible with Protocol Buffers, WebAssembly, and DWARF. - Zigzag. Signed-to-unsigned mapping so negative deltas produce short
varints.
const fnend-to-end. - Bit-level cursors. [
BitReader] and [BitWriter] pack arbitrary- width fields, MSB-first, into and out of a caller-supplied byte buffer. - Framing strategies. Stateless [
LengthPrefixed] framer (u8 / u16 / u32 width, big or little endian, configurable payload cap) and [Delimited] framer (single- or multi-byte delimiters with a bounded scan window). Both implement a common [Framer] trait, so callers can swap strategies without rewriting protocol code. no_stdby default. Thestdfeature only enables animpl std::error::Error for Error. Drop it for embedded targets.- Zero runtime dependencies. The full crate compiles standalone.
- Stable API. From 1.0 onward, the public surface is frozen. Subsequent 1.x releases are bug fixes and non-breaking additions.
Quick start
[]
= "1"
use WriteBuf;
use ;
let framer = new;
let mut out = ;
let mut buf = new;
framer.write_frame.unwrap;
let n = buf.position;
let frame = framer.next_frame.unwrap.unwrap;
assert_eq!;
The full reference lives in docs/API.md. Runnable
demonstrations are under examples/:
Usage patterns
Length-prefixed framing
Each frame is preceded by a fixed-width unsigned integer giving the payload length. The width and byte order are chosen at construction.
use WriteBuf;
use ;
// 16-bit big-endian length, capped at 4 KiB per frame.
let framer = new
.with_max_payload;
let mut out = ;
let mut buf = new;
framer.write_frame.unwrap;
let frame = framer.next_frame.unwrap.unwrap;
assert_eq!;
assert_eq!; // 2-byte header + 5-byte payload
Delimited (line-oriented) framing
Each frame is terminated by a configured delimiter byte sequence. The
delimiter is stripped from the emitted payload but counted in
Frame::consumed.
use ;
let framer = new.unwrap;
let request = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\nbody";
let line = framer.next_frame.unwrap.unwrap;
assert_eq!;
assert_eq!;
Compact integers (varint + zigzag)
varint encodes any u32 / u64 in one to ten bytes. Pair it with
zigzag to encode signed values whose typical magnitude is small.
use ;
let mut out = ;
let mut buf = new;
// Encode the timestamp delta -750 as a 2-byte varint.
encode_u64.unwrap;
assert_eq!;
Bit-packed records
BitReader and BitWriter give you bit-level access for protocols where
multiple fields share a byte.
use BitWriter;
let mut storage = ;
let mut w = new;
w.write_bits.unwrap; // 3-bit type tag
w.write_bits.unwrap; // 5-bit value
assert_eq!;
assert_eq!;
Custom framing
Implement [Framer] on your own type to plug in a custom framing strategy
without giving up the rest of the toolkit.
use ;
use ;
;
Performance
Reference numbers measured on the development machine (release profile, single thread, Windows 11). Variation across machines is expected; the numbers are a baseline for regression detection, not a benchmark target.
| Operation | ns/op |
|---|---|
varint::encode_u32 (small value) |
~0.4 |
varint::encode_u32 (u32::MAX) |
~0.7 |
varint::decode_u32 (2 bytes) |
~0.2 |
varint::decode_u32 (5 bytes) |
~0.2 |
varint::encode_u64 (u64::MAX) |
~1.1 |
varint::decode_u64 (10 bytes) |
~1.8 |
zigzag::encode_i32 |
~0.4 |
ReadBuf::read_u32_be |
~1.4 |
WriteBuf::write_u32_be |
~1.5 |
LengthPrefixed::write_frame (64 B) |
~1.6 |
LengthPrefixed::next_frame (64 B) |
~2.0 |
Delimited::next_frame (short line) |
~2.4 |
Delimited::next_frame (1 KiB scan + CRLF) |
~1300 |
Reproduce locally:
The bench harness is a manual std::time::Instant loop with black_box
markers — zero dependencies and MSRV-compatible.
Stability and semver
From 1.0.0 onward, the public API surface is frozen. Subsequent 1.x
releases will only contain:
- Bug fixes (PATCH).
- Performance improvements that do not change observable behavior (PATCH).
- New additions that do not break existing code: new error variants (the
Errorenum is#[non_exhaustive]), new prefix widths (LengthWidthis#[non_exhaustive]), new helper methods on existing types, additional features behind feature flags (MINOR).
Any breaking change requires a 2.0.0 release. The crate follows
Keep a Changelog and
Semantic Versioning.
no_std and feature flags
| Feature | Default | Effect |
|---|---|---|
std |
enabled | Adds impl std::error::Error for Error. The crate works on no_std targets when this feature is disabled. |
To use the crate without std:
[]
= { = "1", = false }
Project layout
wire-codec/
├── src/
│ ├── lib.rs # crate root, re-exports
│ ├── error.rs # Error, Result
│ ├── buf.rs # ReadBuf, WriteBuf
│ ├── traits.rs # Encode, Decode
│ ├── varint.rs # LEB128
│ ├── zigzag.rs # signed-to-unsigned mapping
│ ├── bitfield.rs # BitReader, BitWriter
│ └── framing/
│ ├── mod.rs # Framer, Frame
│ ├── length.rs # LengthPrefixed
│ └── delimiter.rs # Delimited
├── tests/ # smoke, integration, proptest suites
├── benches/ # codec.rs, framing.rs (manual harness)
├── examples/ # runnable demos
├── docs/
│ ├── API.md # full API reference
│ └── release/ # per-version release notes
├── REPS.md # engineering standards (supreme authority)
└── deny.toml # supply-chain policy for cargo-deny
Standards
- REPS governs every decision. See
REPS.md. - MSRV: Rust 1.75.
- Edition: 2021.
- Cross-platform: Linux, macOS, Windows (CI on all three).
- Supply chain:
cargo auditandcargo denyenforced in CI. - Test discipline: unit, integration, property, and doctests.
License
Dual-licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.
Contributions are accepted under the same dual license.