Derive¶
Writing serializing and deserializing code by hand would be tedious and error-prone. The #[derive_shrink_wrap(..)]
attribute generates all of it from a struct or enum definition. From one definition it can:
- implement
SerializeShrinkWrapandDeserializeShrinkWrap(and their..Ownedcounterparts); - generate a borrowed (zero-copy) type, an owned (
std) type, or both:&strbecomesString,RefVec<'i, T>becomesVec<T>, and so on; - keep newer and older versions of a type wire-compatible, with
#[default = ..]on new fields.
This page covers what you need day to day. The derive reference lists every directive, size assumptions explains how to trade evolvability for size, and fixed and sparse layouts covers the special encodings. All examples were checked against the current macro implementation.
Prerequisites¶
The examples assume wire_weaver = "0.5.0" in Cargo.toml (or shrink_wrap alone, with
use shrink_wrap::prelude::*).
Quick start¶
use wire_weaver::prelude::*;
#[derive_shrink_wrap(borrowed, derive(Debug, PartialEq))]
struct CoordV1 {
x: u8,
y: u8,
}
fn derive_on_struct() {
let mut buf = [0u8; 64];
let coord = CoordV1 { x: 0xAA, y: 0xCC };
let bytes = coord.to_ww_bytes(&mut buf).unwrap();
assert_eq!(bytes, &[0xAA, 0xCC]);
let des = CoordV1::from_ww_bytes(bytes).unwrap();
assert_eq!(des, coord);
}
Two things to notice:
- Regular derives (
Debug,PartialEq, ...) go in aderive(..)argument, not a separate#[derive(..)]attribute. The macro can emit more than one type from one definition (a borrowed and an owned one), and a plain#[derive(..)]would only decorate the first. CoordV1has no lifetime and noString/Vecfields, so the macro can't tell whether you meant a borrowed or an owned type. That's whyborrowedis spelled out. See the next section.
Borrowed, owned, or both?¶
The macro looks at the fields and classifies the type into one of three kinds:
| Kind | Detected when | What gets generated |
|---|---|---|
| Implied borrowed | A field has a lifetime (&'i str, RefVec<'i, T>, UserType<'i>, ...) and no String, Vec or Box |
The type as written is the borrowed type. With owned, {Name}Owned is generated too, by mapping borrowed types to owned ones. |
| Explicit owned | The name ends in Owned and no field has a lifetime |
The type as written is the owned type. With borrowed, {Name} (without Owned) is generated by mapping back. |
| Ambiguous | Every field is a plain type (u8, bool, another plain type, ...) |
Exactly one type, no renaming. Say with borrowed and/or owned which traits it implements. |
Mixing the two styles (a lifetime and a String, or an Owned name with a lifetime) is a compile error asking
you to pick one.
// Implied borrowed: has a lifetime, no `owned` -> only `Coords<'i>` is generated.
#[derive_shrink_wrap]
struct Coords<'i> {
points: RefVec<'i, u8>,
}
// Ambiguous: no lifetime, no alloc type -> say which trait(s) to derive.
#[derive_shrink_wrap(borrowed, derive(Debug, PartialEq))]
struct CoordV1 {
x: u8,
y: u8,
}
Zero-copy and owned types¶
The common case: firmware serializes a borrowed type into a buffer without an allocator, the host reads it as an
owned type, or the other way round. Typing two definitions by hand would be error-prone; owned generates the
owned one, usually behind a feature:
#[derive_shrink_wrap(owned(feature = "std"), derive(Debug, PartialEq))]
pub struct FullVersion<'i> {
pub crate_id: &'i str,
}
With feature = "std" on, this also generates FullVersionOwned { pub crate_id: String } with the same derives,
and serdes code that writes and reads exactly the same bytes:
fn round_trip() {
// on no_std, no allocator
let v = FullVersion { .. };
let bytes = v.to_ww_bytes(&mut buf).unwrap();
// on host, with allocator
let v_owned = FullVersionOwned::from_ww_bytes_owned(bytes).unwrap();
assert_eq!(v.to_owned(), v_owned);
let bytes_from_owned = v_owned.to_ww_bytes_owned().unwrap();
assert_eq!(bytes, bytes_from_owned);
// again on no_std
let v_ref = FullVersion::from_ww_bytes(bytes_from_owned).unwrap();
assert_eq!(v, v_ref);
}
Type mapping¶
| Borrowed type | Owned equivalent |
|---|---|
RefVec<'i, T> |
Vec<T> |
&'i str |
String |
RefBox<'i, T> |
Box<T> |
MaxLen<RefVec<'i, T>, N> |
MaxLen<Vec<T>, N> |
MaxLen<&'i str, N> |
MaxLen<String, N> |
Series<'i, T> |
SeriesOwned<T> |
UserType<'i> |
UserTypeOwned |
UserType<'i, T> |
UserTypeOwned<T> |
Generic arguments of a user type are emitted as written (Twice<u8> stays Twice<u8> on both sides); only a
leading lifetime is replaced by the generated type's, and dropped in the owned type. Everything else (numbers,
arrays, Option, TailSize, ...) is the same on both sides.
Evolving types¶
#[default = ..] on a field marks it as added in a later version: newer and older versions of the type read each
other's data. Fields with a default go after all the others.
#[derive_shrink_wrap(borrowed, derive(Debug, PartialEq))]
struct CoordV1 {
x: u8,
y: u8,
}
#[derive_shrink_wrap(borrowed, derive(Debug, PartialEq))]
struct CoordV1_1 {
x: u8,
y: u8,
#[default = None]
z: Option<u8>,
}
fn evolved_struct() {
let mut buf = [0u8; 64];
let coord = CoordV1_1 { x: 0xAA, y: 0xCC, z: Some(0xFF) };
let bytes = coord.to_ww_bytes(&mut buf).unwrap();
assert_eq!(bytes, &[0xAA, 0xCC, 0x80, 0xFF]);
// newer type from older data
let coord: CoordV1_1 = CoordV1_1::from_ww_bytes(&[0xAA, 0xCC]).unwrap();
assert_eq!(coord, CoordV1_1 { x: 0xAA, y: 0xCC, z: None });
// older type from newer data
let old_coord: CoordV1 = CoordV1::from_ww_bytes(bytes).unwrap();
assert_eq!(old_coord, CoordV1 { x: 0xAA, y: 0xCC });
}
This works as long as the type stays evolvable, which is the default (see size assumptions for the alternatives).
A type that also derives serde::Deserialize (with derive(..) or cfg_attr(cond, derive(..))) gets
#[serde(default = ..)] on each such field, so serde data written before the field was added (a RON or JSON file
saved by an older version) reads it as its default too, instead of failing with "missing field".
A field with a default reads as its default only when it starts at the end of the data and its read runs out of
data. A field that starts with data left is read as usual: a truncated or malformed value is an error, never
replaced with the default. The default can be any expression of the field's type (#[default = 7],
#[default = Vec::new()], #[default = Default::default()]).
Padding bits read as 0
The last byte of older data can have unused bits (the rest of a byte after a bool, or the nibble next to a
size at the back of the buffer). A new bit-sized field there (bool, nibble, the flag of an Option or
Result) is read from them as 0, and newer data packs it the same way. So give such fields their zero value as
the default: false, None. An Option with #[default = Some(..)] reads as None or the default depending
on where the old data ends.
See the evolution rules for every change that stays compatible, and the evolution checker to verify it across versions.
Tuple and unit structs¶
struct Celsius(f32);, struct Pair<'i>(u8, &'i str); and struct Marker; take the same directives as a struct
with named fields and are written the same way, field after field. New fields go at the end with
#[default = ..]. Their fields are _0, _1, ... wherever the derive needs a name; #[flag] needs named fields.
See built-in types.
Enums¶
Every enum needs ww_repr: the wire type of its discriminant. Pick the smallest that fits the variants you expect
to ever need: u1, u2, ... (bits), nib, u8 / u16 / u32, or unib32 (variable length, room to grow).
#[derive_shrink_wrap(borrowed, derive(Debug, PartialEq), ww_repr = unib32)]
enum Command<'i> {
Stop,
Move { x: i16, y: i16 },
Say(&'i str),
}
Variants can be unit, struct-like or tuple-like. Discriminants are numbered in order from 0; new variants go at the
end. Every enum gets fn discriminant(&self). More in the reference.
Enum variants from a newer version¶
A reader that gets a discriminant it doesn't know would fail with Error::EnumFutureVersionOrMalformedData. So
every evolvable enum (the default) gets an extra variant without writing it (SW-47): the derive adds Unknown(u32)
as if it were written last. This
#[derive_shrink_wrap(borrowed, derive(Debug, PartialEq), ww_repr = unib32)]
enum IoError {
InUse,
Refused,
}
is the same type as
#[derive_shrink_wrap(borrowed, derive(Debug, PartialEq), ww_repr = unib32)]
enum IoError {
InUse,
Refused,
/// A variant from a newer version of this type, with its discriminant.
#[future_version]
Unknown(u32),
}
An older host then reports a newer device's error instead of failing to decode the reply, and can pass the value on. How this fits with the rest of an API's errors: Errors.
- The variant takes no number on the wire: the next version adds
NoIoLevelas 2 before it, and an older reader getsUnknown(2). - The enum's bytes and sizes are the same as without it, so it can be added in any version.
- New variants may carry data only in an evolvable enum: its size slot lets older readers skip their fields.
- Writing
Unknown(d)writes the discriminantdwithout fields. discriminant()returns the wire discriminant, also forUnknown(d).
To leave it out (an enum that is complete by nature, like Parity) or rename it, see
no_future_version.
Generic types¶
Structs and enums take type and const parameters and where clauses like any Rust type (DER-14):
#[derive_shrink_wrap(borrowed, owned, final_structure, derive(Debug, PartialEq))]
struct Timed<T> {
t: u32,
value: T,
}
#[derive_shrink_wrap(owned, sized, derive(Debug, PartialEq))]
struct Buf<const N: usize> {
len: u8,
data: [u8; N],
}
#[derive_shrink_wrap(borrowed, owned, ww_repr = u4, derive(Debug, PartialEq))]
enum Reading<'i, T: Copy> {
Value(T),
Missing,
Labeled { label: &'i str, value: T },
}
The parameters' sizes are part of the type's: Buf { len: 2, data: [1, 2, 3] } is the four bytes 02 01 02 03, and
a Buf<3> is sized at 32 bits. The derive adds the serialization traits as bounds on each impl, your own bounds
and where clause stay as written. More in the reference; in an API they are used with
their arguments, see generic types in an API.
Directives at a glance¶
#[derive_shrink_wrap(directive, directive, ..)] takes a comma-separated list. The ones you'll meet most:
| Directive | What it does | Details |
|---|---|---|
borrowed, owned, owned(feature = "std") |
which types to generate | above |
derive(..) |
regular derives on the generated types | reference |
ww_repr = .. |
an enum's discriminant type | reference |
sized, final_structure, self_describing |
smaller, but can't be evolved | size assumptions |
display |
Display and Error from doc comments, for error types |
reference |
validate = path |
reject out-of-range values on read | reference |
fixed_layout, head_tail, sparse |
other encodings: constant offsets, a C-friendly head and tail, sparse fields | layouts |
The derive reference has all of them, including cfg_attr(..), #[cfg] on variants,
discriminants, crate_path and the field attributes.
Next steps¶
- Built-in types: what can be a field.
- API overview: use these types in methods, properties and streams.