Skip to content
Basic

Built-in types

Every type on this page implements the ShrinkWrap traits: it can be a field of a #[derive_shrink_wrap] type, and (unless noted) an argument, return value, property or stream item of an API. Sizes below are for the value on its own; how values share bytes is on the ShrinkWrap page.

At a glance

Type On the wire Alignment
bool 1 bit bit
U1..U64, I2..I64 exactly that many bits bit
Nibble 4 bits nibble
u8..u128, i8..i128 1 to 16 bytes, little-endian byte
f32, f64 4 / 8 bytes, IEEE 754, little-endian byte
UNib32 1 to 11 nibbles, small values short nibble
UVlq32, UVlq64 1 to 5 / 1 to 10 bytes, small values short byte
&str, String UTF-8 bytes, length at the back byte
RefVec<'i, T>, Vec<T> the elements, count at the back byte
MaxLen<T, N> same as T, at most N elements as T
[T; N] the elements, no count as T
Option<T>, Result<T, E> a 1-bit flag, then the value bit
tuples (2 to 8), Range<T>, RangeInclusive<T>, () the parts one after another (() is nothing) as parts
RefBox<'i, T>, Box<T> the value and its size byte
structs and enums the fields; an enum's discriminant in 1 bit to 4 bytes as fields
Fixed<U, P, T, B> exactly its number T; unit, scale and bounds in the type as T

The special types (TailSize, ArrayString / ArrayVec, compressed sequences, OptionAnyVec) are listed at the end.

Booleans and numbers

bool takes one bit. Bit-sized values pack back to back; the next byte-sized value starts a fresh byte.

u8..u128, i8..i128 are written at full width, little-endian, starting on a byte: 0x1234u16 is 34 12. f32, f64 are their IEEE 754 bits, little-endian: 1.5f32 is 00 00 c0 3f.

U1..U64, I2..I64 (shrink_wrap::un, in the prelude; u7, ub8, i12... in an API) take exactly as many bits as their name says. In Rust each is backed by the smallest standard integer, with checked constructors (U7::new(x), U7::try_from(x)) and .value() / u8::from(..). A (U3, U7) of (5, 100) is 10 bits: b9 00. Note that u8 is byte-aligned, while U8 is bit-aligned. They work as struct fields, enum variant payloads and API arguments: an enum I2cAddress { Seven(U7), Ten(U10) } with ww_repr = u1 takes 8 or 11 bits (see the showcase).

Nibble (nib in an API) is 4 bits on a 4-bit boundary.

Variable-length numbers take fewer bytes for small values:

Type Encoding Range 300 is
UNib32 3 bits per nibble, the top bit says "more follows" u32 cd 4
UVlq32 big-endian VLQ, 7 bits per byte, the top bit says "more follows" u32 82 2c
UVlq64 the same as UVlq32, 1 to 10 bytes u64 82 2c
UN, IN the bit width (3 to 6 bits), then that many bits u64, i64

UNib32 is what ShrinkWrap uses for lengths and for ww_repr = unib32 discriminants; values below 8 take one nibble (showcase). UVlq32Backfill is a UVlq32 always written as 5 bytes, so its value can be filled in after the rest is written.

Strings and lists

&'i str (borrowed, zero-copy) and String (std) are UTF-8 bytes, with the length at the back of the buffer as a UNib32 (why at the back): "abc" on its own is 61 62 63 03. Invalid UTF-8 fails to read with Error::MalformedUtf8.

RefVec<'i, T> (borrowed, no allocator) and Vec<T> (std) are the elements one after another, with the count at the back. A RefVec read from a buffer points into it and decodes elements as you iterate; to write one, make it from a slice (RefVec::Slice { slice: &items }, or RefVec::new_bytes(&[1, 2, 3]) for bytes). The derive macro maps RefVec to Vec and &str to String for the owned type (type mapping). Without a slice, e.g. elements coming from a sensor loop, RefVecBuilder writes a list into the buffer one element at a time, with the same bytes.

[T; N] is N elements with no count. N can be a literal or any const expression ([f32; MAX_SERIES]).

To own a string or list without an allocator, use ArrayString<N> / ArrayVec<T, N>: the same bytes, stored in a fixed-size array.

Bounded lists and strings: MaxLen<T, N>

MaxLen<T, N> wraps a list or a string (RefVec<'i, T>, Vec<T>, &'i str or String) and allows at most N elements (bytes for a string). It works everywhere a type can appear: struct and enum fields, method arguments and return types, properties, stream! and sink! items, and nested inside other lists (MaxLen<RefVec<'i, MaxLen<&'i str, 8>>, 4>).

pub const MAX_PIECE: usize = 256;

#[ww_trait]
trait Stage {
    fn write(offset: u32, last: bool, data: MaxLen<RefVec<'i, u8>, MAX_PIECE>);
    property!(rw label: MaxLen<&'i str, 32>);
}

In a #[ww_trait], N is an integer literal, {literal}, or the name of a const defined in the API crate's lib.rs. In a plain #[derive_shrink_wrap] type it is any const expression.

  • Checked both ways, never truncated. MaxLen::new refuses a value that is too long. Serializing one (e.g. after changing it through DerefMut) fails with Error::MaxLenExceeded, and so does deserializing one, before any element is read. A server answers an over-long argument with an error, a client refuses to send one.
  • Same bytes as the unbounded type. The bound only exists in the code on both ends. That keeps it evolvable: a bound can be added, raised, lowered or removed without a new wire format, and the evolution rules say which of those old peers accept.
  • Visible in introspection. ww_self describes it as Type::MaxLen { max_len, ty }, so the dynamic client and the Python module check the bound too (Python raises ValueError: ... at most N ...).

MaxLen bounds the length of one value. The number of resources in a resource array (stream!(adc[]: u16)) is a different thing, see arrays.

Worst-case size at compile time

Every type has a SerializeShrinkWrap::MAX_SIZE: Option<MaxSize>:

  • Some when every part of it is bounded: numbers, arrays, Option / Result, structs and enums of them, and MaxLen lists and strings of bounded elements;
  • None as soon as anything can grow without a limit: a plain Vec, RefVec, String, RefBox, compressed sequences.

MaxSize::buffer_len_of(T::MAX_SIZE) is a buffer length any value of T always serializes into. It is an upper bound, not the exact worst case: every length is counted as at least four nibbles and every alignment as needed. Owned types don't have MAX_SIZE yet (SW-42).

A generated no_alloc server also gets, per method, <METHOD>_REQUEST_MAX_LEN and <METHOD>_RESPONSE_MAX_LEN: Option<usize>: the longest request message that calls it and the longest reply, None when an argument or the return type is unbounded. A firmware can check at compile time that its message buffer is big enough:

const MAX_MESSAGE_LEN: usize = 512;
const _: () = assert!(matches!(
    api_server::WRITE_REQUEST_MAX_LEN,
    Some(len) if len <= MAX_MESSAGE_LEN
));

Option and Result

Option<T> is a 1-bit flag (1 for Some), then the value: Some(5u8) is 80 05, None is one zero bit. Result<T, E> is the same with 1 for Ok and 0 for Err, followed by the T or the E. Both work inside lists, as fields and in every API position. Where the flag goes can be moved with #[flag] (showcase), and why a flag beats an enum tag is in the showcase too.

Tuples, ranges and ()

Tuples of 2 to 8 elements, Range<T> and RangeInclusive<T> are their parts one after another, as if they were a struct's fields. () takes no bytes, for a method that returns nothing.

Self-referential types: RefBox

A type that contains itself (a tree, an expression) needs indirection. RefBox<'i, T> gives it without an allocator: read from a buffer it points into it and decodes on .read(); its owned counterpart is Box<T>. On the wire it is the value plus its size, so a reader can jump over it. See the showcase.

Such types work anywhere in an API (API-44), as arguments, return values, properties and stream items, also in the dynamic client, ww and Python:

#[ww_trait]
trait Calc {
    fn eval(expr: Expr<'i>) -> Option<i32>;
    property!(rw tree: Tree<'i>);
}

#[derive_shrink_wrap(borrowed, owned(feature = "std"), derive(Debug, PartialEq, Clone), ww_repr = u4)]
pub enum Expr<'i> {
    Add(RefBox<'i, Expr<'i>>, RefBox<'i, Expr<'i>>),
    Neg(RefBox<'i, Expr<'i>>),
    Lit(i32),
}

#[derive_shrink_wrap(borrowed, owned(feature = "std"), derive(Debug, PartialEq, Clone))]
pub struct Tree<'i> {
    pub label: &'i str,
    pub children: RefVec<'i, Tree<'i>>,
}

The API model has each such definition once, referring to itself. As in Rust, every cycle goes through a RefBox or a list. Types with parameters can't be part of a cycle (struct List<T> { next: Option<RefBox<'i, List<T>>> } is an error), a type without them can use itself as an argument (children: RefVec<'i, Timed<Tree<'i>>>). A value is as deep as its bytes say; decoding it is bounded: the reader stops at its depth limit, the dynamic client at about 70 levels, so a malformed or malicious value is an error, not a stack overflow on a host.

Recursive types in firmware

Generated server code reads arguments with the default limit of 128 levels, which a small MCU stack may not hold: a host can send a value that deep. Until the server takes a lower limit, keep recursive types out of the arguments of methods and properties a device serves, or give its task a stack large enough for 128 levels.

Structs and enums

Your own types, with #[derive_shrink_wrap]:

  • Structs are their fields in order. By default a struct is evolvable: fields can be appended later. Tuple and unit structs work too, see below.
  • Enums, with or without data in their variants, are a discriminant followed by the variant's fields. The discriminant takes what ww_repr says: u1, u2, ... (bits), nib, u8 / u16 / u32 (bytes), or unib32 (variable length).

Tuple structs

A tuple struct is written exactly like a struct with the same fields named: Celsius(21.5) is the f32 alone, 00 00 ac 41, and Rgb(1, 2, 3) is 01 02 03.

A unit struct (struct Marker;) has no data, but inside a parent it records its length like any evolvable struct, a nibble that is usually shared with a neighbour: (1, Marker, 2) is 01 02 00. That is what makes it useful in an API: fn arm() -> Armed can later become Armed { at: Timestamp }, and older code still reads it. On its own (a whole message) nothing follows it, so it takes no bytes at all. A unit struct with sized, final_structure, self_describing or fixed_layout would be zero bits that can never change, so the derive refuses it: leave the size assumption out, or give it reserved bits to grow into (e.g. a U4 field).

#[derive_shrink_wrap(borrowed, owned, derive(Debug, Clone, Copy, PartialEq))]
pub struct Celsius(pub f32);

#[derive_shrink_wrap(borrowed, owned, sized, derive(Debug, Clone, Copy, PartialEq))]
pub struct Rgb(pub u8, pub u8, pub u8);

#[derive_shrink_wrap(owned, derive(Debug, Clone, PartialEq))]
pub struct Pair<'i>(pub u8, pub &'i str); // and PairOwned(u8, String)

Everything a named struct can do works the same way: size assumptions, fixed_layout, head_tail, sparse, validate, display (with {0}, {1}), and new fields with #[default = ..], only at the end. Where the fields need a name (layout constants such as Rgb::_2_OFFSET, C names, error messages) they are _0, _1, ... The one thing that needs names is #[flag], which pairs a relocated flag with its field by name: name the fields to use it. Dynamic clients see the fields unnamed, Python as a tuple (a single field as its value, a unit struct as None).

How much a nested struct or enum costs on top of its fields depends on its size assumption: an evolvable one adds its length (usually a nibble), a sized one nothing.

Generic types in an API

A generic struct or enum is used in an API with its arguments, in every position: arguments, return values, properties, stream items, fields of other types (API-43).

#[ww_trait]
trait Sensor {
    fn swap(pair: Pair<u8, u16>) -> Pair<u16, u8>;
    fn sum(window: Window<4>) -> u32;
    property!(ro current: Timed<Ampere>);
    stream!(readings: Reading<i16>);
}

#[derive_shrink_wrap(borrowed, owned, derive(Debug, Clone, Copy, PartialEq))]
pub struct Window<const N: usize> {
    pub samples: [u16; N],
}

Generated server and client code uses the types as written (Window<4>, ww_si::Timed<ww_si::Ampere>, a Tagged<'i, T> as TaggedOwned<String> on the owned side). The API model has each generic definition once, with its parameters, and each use as an instance with its arguments; ww_si::Timed<T> is known from the ww_si snapshot, so a device leaves its definition out of its introspection data like that of any known type. The dynamic client, ww and Python see the concrete types (Timed<Ampere>, Window<4>) and read and write the same bytes as the derived ones.

Arguments can be types, integer and bool literals, const items of the crate, a const parameter passed on (struct Outer<const N: usize> { window: Window<N> }), or left to the parameter's default. How a generic type may change is in the evolution rules.

Type aliases

A type alias stands for what it names, in every API position (API-45):

#[ww_trait]
trait Supply {
    property!(ro voltage: Volt);
    property!(ro sampled: Timed<Volt>);
    fn last(n: u8) -> Reading<u32>;
}

pub mod units {
    pub struct Volt;
}
pub type Volt = Quantity<units::Volt>;
pub type Reading<T> = Pair<Window<2>, Option<T>>;

Aliases are found like structs and enums: in lib.rs, in modules, through use imports (renamed ones too, use units::Volt as V) and globs, also in other crates. A generic alias puts its arguments where its parameters are, a const parameter too (type Buf<const N: usize> = Window<N>). The API model has no entry for the alias itself: voltage is a Quantity<Volt> there (the marker units::Volt), and ww, the dynamic client and Python show it that way. A PhantomData<U> field is left out of a struct's definition (it takes no bits on the wire), so U is a parameter that no field uses.

Types in modules

A type the API uses can live in any module of its crate. Each definition records the path other crates name it by: the shortest one through pub mods and pub use re-exports (API-46). In the example above, that is my_api::Volt for the alias and my_api::units::Volt for the marker; a type in a private module re-exported at the root with pub use ty::Ty; is my_api::Ty, and one re-exported under another name (pub use ty::Inner as Outer;) is my_api::Outer, also in ww and the dynamic client. A type that other crates can't reach at all is an error that names it. The <Name>Owned variant of a type with a lifetime is named next to it: re-export it along with the type (pub use ty::{Label, LabelOwned};, or a glob).

The path and the name are the definition's identity: units::Volt and a Volt at the crate root are two different types, in generated code, in API snapshots (ww api save takes the types of pub mods too, unit marker structs like pub struct Volt; included) and in introspection data.

Types from crates that aren't API crates

An API can use types of any crate, also one that isn't built on WireWeaver: no wire_weaver dependency, no #[ww_trait], only data types with #[derive_shrink_wrap] and shrink_wrap as its dependency (API-60). The crate walker reads its source like an API crate's, and ww api save snapshots its types and unit markers.

Such a crate usually keeps shrink_wrap optional, so that users who don't need the wire format don't build it. The derive and its field attributes then sit behind a feature:

#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "shrink_wrap", derive_shrink_wrap(borrowed, owned(feature = "std")))]
pub struct Reading {
    pub value: Milli<units::Volt>,
    #[cfg_attr(feature = "shrink_wrap", default = 1)]
    pub samples: u8,
}

The API crate turns the feature on, in its dependency entry (exact_units = { version = "..", features = ["shrink_wrap"] }) or through its own [features] (wire = ["exact_units/shrink_wrap"]). The walker reads the crate's #[cfg_attr(..)]s as rustc does, with the features Cargo builds it with: those its dependents ask for (in [dependencies], [workspace.dependencies] plus the member's own, and dep/feature), its default features unless default-features = false, and what those enable in its [features] table. A crate reached from several dependents is read once with the union of what they ask for, whichever the walker reaches first. ww api save reads a crate with all its features, so its snapshot has every gated type.

On a field or an enum variant, a cfg_attr holding attributes the derive reads (default, wire, flag, ..) is read as if its predicate holds, by the derive and the walker alike, and the type fails to compile where it doesn't: the bytes never depend on a feature (see the derive reference). Use the same feature as the type's derive. #[cfg(..)] on items isn't evaluated by the walker: a type is found as if its cfg held.

The API model names a type by the crate that defines it, so generated code does too. When the crate with the ww_codegen! call doesn't depend on that crate itself, generated code names it through a crate it does depend on that re-exports it at the root:

// in ww_si: its quantities are defined in exact_units
pub use exact_units;

A server depending on ww_si alone then gets ww_si::exact_units::units::Second in its generated code, while introspection data keeps exact_units::units::Second. pub use dep as name; and pub extern crate dep; work the same, through any number of crates. Without such a re-export, the generated code names the crate directly, and the crate with the ww_codegen! call needs a dependency on it.

Special types

For particular jobs, on their own page:

Type For
TailSize<N> a value that bounds itself inside a longer buffer (file headers, records)
ArrayString<N>, ArrayVec<T, N> owned strings and lists without an allocator
Series<T> of numbers compressed time series of integers, timestamps and floats
OptionAnyVec, EitherAnyVec sparse sets of values of any types; a sparse struct is built on it
TailBytes bytes that take up the rest of the buffer, no length; generated API code uses it for byte slices

These are for Rust types and generated code; an API definition (#[ww_trait]) doesn't name them. The compressed sequences are the exception: they work in every API position.

Library types

The standard library has many more types built from these: date and time, versions, numbers with units, SI quantities, and the types of its I/O traits.

Not supported yet

  • char and maps (HashMap, BTreeMap; for now a Vec of (K, V), SW-26).
  • Numbers restricted to a range or a set (SW-20), like u16 in 1..=512 or u8 in 0..=8, 12, 16, checked on both ends. Until then, a struct can check its fields with validate.
  • Units on plain numbers (f32 in amperes) as a hint for code generation and tools, not sent over the wire. Today a number with its unit in the type is an exact_units::Fixed (Micros<u32>: the bytes of the u32, µs in introspection), and ww_si has SI quantities whose prefix and number type the device picks.