pdk-serde-fixint
A small, self-owned serde codec that is byte-for-byte compatible with the
bincode 1.3 default configuration (little-endian, fixed-width integers, u64
length prefixes, u32 enum variant tags, reject-trailing-bytes on decode).
Why this crate exists
bincode 1.3 is end-of-life and must not appear in the runtime dependency graph
of any built policy. But the PDK persists state (rate-limit buckets, cache
entries, lock values, connection counts, …) with bincode's wire format, so the
replacement has to read bytes that were already written by the old code.
serde_fixint reproduces that exact wire format in-tree. The legacy bincode
crate is kept only as a dev-dependency to prove compatibility — see
tests/interop.rs, which asserts, for every representative PDK type, that:
- our encoder produces the exact bytes bincode produces,
- bincode decodes our bytes, and
- we decode bincode's bytes.
Usage
let bytes = to_vec?;
let value: T = from_slice?;
Performance
benches/codec.rs (criterion, dev-dependency only) compares serde_fixint
against bincode 1.3 over PDK-representative wire types. Run with:
Median times below, Apple M4 Max, rustc 1.88.0, --release. fixint is this
crate; bincode is the legacy reference. Absolute numbers are machine-specific;
the ratio is the point of interest.
| Payload | Op | fixint | bincode | fixint / bincode |
|---|---|---|---|---|
u32 |
encode | 9.55 ns | 9.39 ns | 1.02× |
u32 |
decode | 0.42 ns | 0.40 ns | 1.05× |
SystemTime |
encode | 15.29 ns | 15.74 ns | 0.97× |
SystemTime |
decode | 2.45 ns | 2.47 ns | 0.99× |
Option<SystemTime> |
encode | 15.45 ns | 15.64 ns | 0.99× |
Option<SystemTime> |
decode | 3.44 ns | 4.73 ns | 0.73× |
String (34 B) |
encode | 15.23 ns | 15.27 ns | 1.00× |
String (34 B) |
decode | 17.67 ns | 18.74 ns | 0.94× |
Bucket (1 limit) |
encode | 14.54 ns | 18.10 ns | 0.80× |
Bucket (1 limit) |
decode | 17.39 ns | 16.67 ns | 1.04× |
Bucket (16 limits) |
encode | 55.89 ns | 81.23 ns | 0.69× |
Bucket (16 limits) |
decode | 66.21 ns | 106.0 ns | 0.62× |
Bucket (128 limits) |
encode | 312 ns | 552 ns | 0.56× |
Bucket (128 limits) |
decode | 356 ns | 707 ns | 0.50× |
Reading these numbers
serde_fixint is at parity with bincode 1.3 on scalars and faster on
compound values — the Bucket types (the largest structured payloads the PDK
persists) encode and decode in roughly half bincode's time. Everything is at
nanosecond-to-microsecond scale; in a policy this is dwarfed by the WASM host
boundary, shared-data/Redis round-trips, and network I/O.
The two cases that read as slightly behind — u32 decode (~1.05×) and
Bucket (1 limit) decode (~1.04×) — are sub-nanosecond differences (tens of
picoseconds and ~0.7 ns). At that scale they sit at the timer's noise floor:
bincode's own u32 decode median swings ±15% run-to-run, larger than the gap
itself. They are not a meaningful algorithmic difference.
Three implementation choices close the gap that a naive serde codec would leave:
- Encode pre-sizing.
to_vecwalks the value once with a size-only pass (size.rs, integer adds, no writes), then encodes into aVecallocated to that exact capacity — so the encode pass never reallocates. This mirrors what bincode 1.3 does internally and is what makes compound encode competitive: without it, aVec::new()encode pays repeated grow+memcpy. On compound values fixint then beats bincode because its size pass is leaner than bincode's generic one. - Copy-free fixed-width reads. The decoder reads each multi-byte integer
via
split_first_chunk::<N>(), feeding an array reference straight intofrom_le_byteswith a single bounds check and no intermediate copy. Hot ser/de methods are#[inline]. - Direct single-byte reads. Single bytes —
Option/enum tags andbool, hit on every value — read viasplit_first()rather than the const-generic chunk path, which the compiler turns into tighter code. This is what makesOption<SystemTime>decode beat bincode rather than trail it.
The output is wire-identical to bincode (the bench asserts equal bytes before timing), so old persisted state stays readable, and the whole thing removes an end-of-life dependency from every policy's runtime graph.