Skip to content

Repository files navigation

tinyklv - the fastest KLV framework in Rust

Crates.ioDocumentationLicense: MITMSRV

The fastest derive-macro framework for encoding and decoding Key-Length-Value (KLV) binary streams, built on winnow parser combinators.

tinyklv vs the KLV/TLV crates

KLV (a generic Tag-Length-Value framing) is the backbone of telemetry packets, video metadata streams, IoT sensor framing, and most custom binary protocols that evolve without breaking older parsers. tinyklv is protocol-agnostic and ships no baked-in standards: you declare your keys, length encoding, sentinel, and per-field codecs as attributes on a struct, and #[derive(Klv)] generates the encoder and decoder in a single pass.

Quickstart

cargo add tinyklv

Example

use tinyklv::Klv;use tinyklv::prelude::*;use tinyklv::dec::binary as decb;use tinyklv::enc::binary as encb;#[derive(Klv,Debug,PartialEq)]#[klv( stream = &[u8], sentinel = b"\x47\x48", key(dec = decb::u8, enc = encb::u8), len(dec = decb::u8_as_usize, enc = encb::u8_from_usize),)]structHeartbeat{#[klv( key = 0x01, dec = decb::u8, enc = *encb::u8,)]sequence:u8,#[klv( key = 0x02, dec = decb::be_u16, enc = *encb::be_u16,)]temperature_centideg:u16,}fnmain(){let original = Heartbeat{sequence:42,temperature_centideg:2350,};letmut frame = Vec::new();
original.encode_frame(&mut frame);let decoded = Heartbeat::decode_frame(&mut frame.as_slice()).unwrap();assert_eq!(decoded, original);}

Full annotated version: examples/01_hello_world.rs.

Speed

Across decode and encode, flat and nested, clean and framed-over-noise, tinyklv is the fastest derive-based KLV framework - several times faster than serde_klv, an order of magnitude faster than tlv_parser, and within striking distance of hand-rolled parsing. It is also the only one that handles nested KLV without hand-written glue.

The suite also pits all four KLV approaches against four protobuf stacks (prost, quick_protobuf, rust_protobuf, micropb) encoding the same records from .proto schemas - a different wire format (length-prefixed tag-value rather than KLV), measured the same way.

A third record is built from common native Rust types (chrono::DateTime, std::net::Ipv4Addr/Ipv6Addr, std::time::Duration, NaiveDate/NaiveTime, char, NonZeroU32, bool, String). tinyklv decodes straight into them via its bench-gated DecodeValue/EncodeValue impls; every other approach must convert from a raw scalar - the KLV libraries field-by-field, and the protobuf crates over a whole generated struct (a generated message can never be a native type), each using whatever native conversions the crate genuinely provides.

The benchmark suite, the eight competing implementations (four KLV libraries and four protobuf crates), and the one-command chart reproduction (benches/scripts/gencharts.sh) all live in benches/.

Benchmark machine:

  • OS: Ubuntu 24.04.4 LTS, Linux 6.17.0-35-generic
  • CPU: AMD Ryzen 9 9900X
  • Host: B850 AI TOP -CF-WCP-ADO

See the full chart set: KLV/TLV · protobuf

The same benchmark records also put tinyklv within protobuf territory, and in several measured encode/decode cases ahead of the protobuf stacks:

tinyklv vs the protobuf stacks

Maintainability

The only thing faster per packet is the manual bar - and it is the least maintainable code in the suite. A hand-rolled decoder is a panic-adjacent slice-indexing loop with one Option per field to juggle, repeated for every record shape, plus a second hand-written sub-parser for every level of nesting. From the benchmark's manual nested decoder (benches/suite/approaches/manual/nested.rs):

fndecode(body:&[u8]) -> Option<Platform>{letmut id = None;letmut coord = None;// ...one `let mut <field> = None;` for every one of nine fields...letmut sensors = None;letmut j = 0;while j + 2 <= body.len(){// bounds checklet tag = body[j];// allow clippy slice indexinglet len = usize::from(body[j + 1]);
j += 2;let val = body.get(j..j + len)?;// hand-rolled bounds math
j += len;match tag {// manually define keys for each struct
key::ID => id = Some(u32::from_be_bytes(val.try_into().ok()?)),
key::COORD => coord = Some(decode_coord(val)?),// a SECOND hand-written loop// ...one arm per field, each with its own try_into().ok()? dance...
_ => {}}}Some(Platform{id: id?,coord: coord?,/* ...unwrap all nine... */sensors: sensors? })}

tinyklv collapses the whole thing - both directions, nesting included - into attributes on the struct:

#[derive(Klv)]#[klv( stream = &[u8], sentinel = b"\x47\x48", key(dec = decb::u8, enc = encb::u8), len(dec = decb::u8_as_usize, enc = encb::u8_from_usize), trait_fallback,)]structPlatform{#[klv(key = 0x01, dec = decb::be_u32, enc = *encb::be_u32)]id:u32,#[klv(key = 0x02)]coord:GpsCoord,// where this struct has `#[derive(Klv)]`// ...the remaining fields, one attribute line each...}

Features

  • Built-in codecs: binary (native/BE/LE for u8..u128, i8..i128, f32/f64), BER length, BER-OID keys, UTF-8 / UTF-16 / ASCII strings
  • Sentinel seeking - resync on noisy byte streams
  • Streaming partial packets on noisy or incomplete streams
  • Repeated decode with user-defined break conditions
  • Nested Klv structs - compose packets from sub-packets
  • Generic structs and lifetimes, Option<T> fields, per-field/container defaults
  • Stream type is user-selected, where any winnow::Stream works

Documentation

Contributing

Issues and pull requests welcome at https://github.com/arpadav/tinyklv. Run cargo test --all and cargo clippy --all-targets -- -D warnings before opening a PR.

License

Licensed under the MIT License. See LICENSE for details.

Support

If tinyklv is useful to you:

About

Tinyklv - the fastest KLV framework in Rust

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages