Skip to content
Basic

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 SerializeShrinkWrap and DeserializeShrinkWrap (and their ..Owned counterparts);
  • generate a borrowed (zero-copy) type, an owned (std) type, or both: &str becomes String, RefVec<'i, T> becomes Vec<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 a derive(..) 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.
  • CoordV1 has no lifetime and no String / Vec fields, so the macro can't tell whether you meant a borrowed or an owned type. That's why borrowed is 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 NoIoLevel as 2 before it, and an older reader gets Unknown(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 discriminant d without fields.
  • discriminant() returns the wire discriminant, also for Unknown(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