wirecodec is a powerful, attribute-driven binary codec framework for Rust. It enables you to define binary-encoded protocol structures declaratively using:
- Bit-fields
- Byte-fields
- Padding
- Length- and count-prefixed fields
- Optional fields with conditions
- Nested structures
- CRC / fingerprint fields
- Verification constraints
- Endian-aware integers
- Zero-copy decoding with lifetimes
The companion crate wirecodec_derive generates highly optimized encode/decode implementations.
wirecodec is designed for real-world networking protocols such as:
- STUN / ICE / TURN
- RTP / RTCP
- TLV-style formats
- NTP
- Custom packet formats
Much of this code has been generated using LLM-assistance. The purpose of this project is mostly to experiment to figure out how well LLM can generate realistically useful code. I have to say I am somewhat stunned about the capabilities it is able to do. And it has taught me quite a lot about prompting and "managing" the work that the LLM does.
Define protocol structures with attribute macros:
#[derive(Codec)]
struct Header {
#[codec(bits = 2, verify = 0b10)]
version: u8,
#[codec(bits = 1)]
padding: u8,
#[codec(bits = 1)]
extension: u8,
}The derive macro generates bit-level decoding/encoding automatically.
All decoding uses &[u8] slices and borrows data directly where possible.
fn decode<'a>(buf: &mut DecodeBuffer<'a>) -> Result<MyStruct<'a>, DecodeError>;
This allows parsing directly from network buffers without allocation.
Support for:
- length_of — encode/decode length prefix automatically
- count_of — encode/decode list length
- when = "expression" — conditional fields
- skip — pad bits or bytes
- verify = VALUE — enforce field constraints
- bytes = N or bits = N — precise control of binary layout
- endian = "big" | "little" — integer endianness
- pad_to = N — byte alignment padding
Nested headers, messages, TLV blocks, and protocol stacks work naturally:
#[derive(Codec)]
struct StunMessage<'a> {
header: StunHeader, // nested
#[codec(count_of = "attributes")]
attribute_count: u16,
attributes: Vec<StunAttribute<'a>>,
}Useful for STUN MESSAGE-INTEGRITY and FINGERPRINT:
#[codec(crc32 = true, crc32_xor = 0x5354554e)]
fingerprint: u32,The derive macro validates:
- Missing referenced fields (length_of, count_of)
- Invalid attribute combinations
- Bit-width overflows
- Forbidden types for bytes = N
- Incorrect when conditions
- And more…
Errors point directly to the offending field.
use wirecodec::fmt_utils::{ByteFormatExt, ByteFormatKind};
use wirecodec::{Codec, CodecTrait, DecodeBuffer, Encode, EncodeBuffer};
#[derive(Codec, Debug, PartialEq)]
struct Packet<'a> {
#[codec(bits = 8)]
kind: u8,
#[codec(bits = 8, length_of = "payload")]
length: u8,
#[codec(bytes = 0, pad_to = 4)]
payload: &'a [u8],
}
fn main() {
let pkt = Packet {
kind: 1,
length: 3, // This will be automatically set, but for equality check has the correct value
payload: &[10, 20, 30],
};
// Encode
let mut enc = EncodeBuffer::new();
pkt.encode(&mut enc).unwrap();
let bytes = enc.as_bytes();
// Encoded to: 01 03 0A 14 1E 00
println!("{}", bytes.fmt_bytes(ByteFormatKind::Hex, 10));
// Decode
let mut dec = DecodeBuffer::new(bytes);
let decoded = Packet::decode(&mut dec).unwrap();
assert_eq!(decoded, pkt);
}#[derive(Codec)]
struct StunHeader {
#[codec(bits = 2, verify = 0)] // zero bits
zero: u8,
#[codec(bits = 2)] // STUN class
class: u8,
#[codec(bits = 12)] // STUN method
method: u16,
#[codec(bits = 16)]
length: u16,
#[codec(bits = 32, verify = 0x2112A442)]
magic_cookie: u32,
#[codec(bytes = 12)]
transaction_id: [u8; 12],
}This decodes directly from a UDP receive buffer without allocation.
[dependencies]
wirecodec = "0.1"or for no_std decode-only:
[dependencies]
wirecodec = { version = "0.1", default-features = false }or for no_std + alloc (decode + encode):
[dependencies]
wirecodec = { version = "0.1", default-features = false, features = ["alloc"] }| Feature | Default | Description |
|---|---|---|
| std | yes | Enables std::error::Error, formatting, etc. Implies alloc. |
| alloc | no | Enables Vec and encoding. Required for Encode and EncodeBuffer. |
| Mode | Decode | Encode | Description |
|---|---|---|---|
| std | [X] | [X] | Full desktop/server use |
| no_std + alloc | [X] | [X] | Embedded RTOS, WASM, etc |
| no_std (decode only) | [X] | [ ] | Tiny embedded / kernel environments |
bits = N
Read/write N bits into an integer field.
bytes = N
Read/write an exact byte sequence into a &[u8] or [u8; N].
verify = VALUE
Field must equal VALUE during decoding.
skip
Skip bits/bytes during encoding and decoding.
length_of = "field"
Automatically populate the length prefix based on another field’s length.
count_of = "field"
Number of items in a vector.
when = "expr"
Conditionally decode or encode an optional field.
pad_to = N
Pad output until reaching byte boundary N.
endian = "big" | "little"
Specify integer byte order.
crc32 = true / crc32_xor
Compute CRC-32 over the preceding fields.
The repository contains:
- Unit tests for the derive crate
- Integration tests
- Fuzz tests (wirecodec_fuzz)
- Benchmarks (benches/)
- Example protocols:
- STUN
- RTP
- NTP
- TLV
- CRC-framed packets
To run all tests:
cargo test- Bit-level operations with minimal branching
- Zero-copy decode path over borrowed slices
- Encode buffer grows efficiently
- Derived code is inlined and optimized by Rust
Benchmarks included in benches/.
- Optional "decode-only" derive mode (skip Encode impl)
- Support for slice-backed fixed-size encoding (no_std no-alloc encode)
- More checksum algorithms
- DSL for multi-variant tagged enums
- Procedural derive for packed enums with overlapping bitfields
Contributions, bug reports, and feature requests are welcome!
If you write an example protocol using wirecodec, feel free to open a PR.
Licensed under the same terms as Rust itself:
- MIT OR Apache-2.0