ShrinkWrap¶
ShrinkWrap is the wire format of WireWeaver: every method argument, return value, property and stream item goes through it. It is dense (values are packed down to single bits), it works on a microcontroller without an allocator, and it lets newer and older versions of a type read each other's data.
It lives in its own crate, shrink_wrap, with no dependency on the rest of WireWeaver. If all you need is a compact
format for files, messages or flash records, you can use it on its own.
A first example¶
Put #[derive_shrink_wrap(..)] on a struct or enum and it gets everything it needs to go on the wire:
use wire_weaver::prelude::*; // or `use shrink_wrap::prelude::*;` for the crate on its own
#[derive_shrink_wrap(owned, derive(Debug, PartialEq))]
struct Sample<'i> {
id: u8,
ok: bool,
label: &'i str,
error: Option<u16>,
}
fn main() {
let sample = Sample { id: 7, ok: true, label: "temp", error: None };
// no_std, no allocator: serialize into a buffer you own
let mut buf = [0u8; 64];
let bytes = sample.to_ww_bytes(&mut buf).unwrap();
assert_eq!(bytes, &[0x07, 0x80, 0x74, 0x65, 0x6d, 0x70, 0x04]);
// read it back without copying: `label` points into `bytes`
let read = Sample::from_ww_bytes(bytes).unwrap();
assert_eq!(read, sample);
// with std: `SampleOwned` has a `String` instead of `&str`, same bytes
let owned = SampleOwned::from_ww_bytes_owned(bytes).unwrap();
assert_eq!(owned.to_ww_bytes_owned().unwrap(), bytes);
}
Seven bytes for the whole value:
| Bytes | What |
|---|---|
07 |
id |
80 |
ok: one bit, the rest of the byte is padding before the next byte-aligned field |
74 65 6d 70 |
"temp" |
04 |
error's None flag (one bit), padding, and the length of "temp" (4) |
Two things stand out: a bool or an Option flag costs one bit, and the string's length is not in front of the
string but at the end of the buffer. Both are explained below.
What you get¶
- Small. Values are packed at 1-bit, 4-bit or 1-byte alignment, lengths take a nibble when they are small, and nothing is stored that the reader already knows (no field names, no type tags).
no_stdand no allocator, also for strings, lists and nested types, for reading and writing. On the host, the same type gets an owned twin (Sample→SampleOwned) withStringandVec, writing the same bytes.- Zero-copy reading. A
&stror a list read from the buffer points into it. - Safe on untrusted data. A malformed buffer is an error, never a panic, and a recursive type nested too deep
is
Error::NestingTooDeepinstead of a stack overflow (nesting depth). - Evolvable. By default a type can get new fields later; old and new code keep reading each other's data (derive: evolving types).
- Many types out of the box: integers of any bit width, variable-length numbers, strings and lists with an
optional maximum length,
Option,Result, tuples, arrays, compressed time series and more (built-in types).
How it stays small¶
Bits are packed. A bool takes one bit, a U3 three bits, an enum with four variants can take two. Byte-sized
values (u8, u32, f32, strings) start on a fresh byte, the unused bits before them stay zero. Those zero bits
are not wasted forever: a later version of a type can put a new bool or Option flag there, and old data reads
as false or None.
Lengths go to the back. A string, a list or a nested type whose size isn't known in advance needs its length stored somewhere. Most formats write it in front of the value, which means knowing the size before writing it. ShrinkWrap writes the value first and puts its length at the end of the buffer, as a variable-length number of nibbles. That keeps serialization a single pass with no copying, and small lengths cost half a byte. The reader picks the lengths up from the back while it walks the data from the front.
Nothing extra by default, less if you ask. A struct is evolvable by default, which costs a length nibble or
two when it is nested in another type. A type that will never change can say so (sized, final_structure,
self_describing) and then costs exactly its fields (size assumptions).
For the exact bytes, bit by bit, see wire format internals.
Next steps¶
| Page | Level | What's there |
|---|---|---|
| Derive | Basic | #[derive_shrink_wrap]: borrowed and owned types, evolving types, enums |
| Built-in types | Basic | Every type that goes on the wire and what it costs |
| Size assumptions | Advanced | sized, final_structure, self_describing: smaller, but frozen |
| Derive reference | Advanced | Every directive of the macro |
| Special types | Advanced | TailSize, ArrayString/ArrayVec, compressed sequences, OptionAnyVec |
| Fixed and sparse layouts | Advanced | fixed_layout, head_tail and sparse: other encodings for special cases |
| Showcase | Advanced | Worked, byte-verified tricks |
| Use cases | Advanced | Real files, frames and messages built with ShrinkWrap |
| ShrinkWrap in C | Advanced | The C reader and writer |
| Wire format internals | Nomicon | The FIFO of lengths, BufWriter/BufReader, ElementSize, exact encodings |
| Code size on small CPUs | Nomicon | What ShrinkWrap costs in flash and why |