Multi requests¶
A multi request (CLT-15) answers several resources in one request/response pair instead of one round trip per resource: several different properties or methods at the same level, or the same property/method across a whole array. Each index is answered on its own, so one bad index never fails the others. Use them to cut round trips on a slow or high-latency link - reading a dashboard's worth of properties, writing several settings at once, backing up or restoring a whole array - not for a single resource, where a plain read/write/call is simpler and already one round trip.
Server side: multi_req = true¶
A server only answers MultiRead, MultiCall and MultiWrite when it opts in:
mod server_impl {
wire_weaver::ww_codegen!(
my_device_api :: MyDevice for super::Server,
server = true, no_alloc = true, use_async = false,
multi_req = true,
);
}
Without it, a server refuses the request. With it, the dispatcher gets one more arm that treats every index of
MultiCall/MultiRead/MultiWrite as a Call/Read/Write of its own: an index that fails is answered with an
error in its own slot and the rest go on, the request never panics on malformed input (an unknown path, a mode the
server doesn't support, a path at the depth limit), and it is answered with EventKind::Value holding an
EitherAnyVec of Either<Error, value>, one per index, in index order.
Cost. This is not free: every handler whose reply can end up inside a multi response gets an extra branch in
its serialization path (write the value directly, or into the shared EitherAnyVec, decided at runtime by one
bool), on top of the multi dispatcher itself. Flash use grows with the number of resources the server has, not
just once for the feature - multi_req = true is an opt-in for this reason, not the default.
MultiIndex: which resources¶
multi_idx: MultiIndex<'i> names the indices at the one path element where they diverge (see
split_common on the client side for how that element is found):
| Variant | Meaning |
|---|---|
List(indices) |
exactly these indices, answered in the given order |
Range(a..b) |
a..b, ascending; a >= b is an empty list, not an error |
Mask32(bits) |
bit i set selects index i, so up to 32 indices in one request |
in_each_array_id: Option<UNib32> is one more path element, the same for every index, right after the varying one
- e.g. pin[] selected by multi_idx, then set_level inside each selected pin. The request's own path is the
common prefix shared by every index (everything before the varying element).
The reply: an EitherAnyVec of results¶
The answer is shrink_wrap::either_any_vec::EitherAnyVec (SW-10),
read with EitherAnyVecIter: the type of each slot (the error type, and the index's own value type) is known to
both sides from the API model, not stored, and the element count is implied by multi_idx.len() rather than
written - EitherAnyVec only stores one bit per element (whether it is Left/error or Right/value) plus the
bytes of whatever each element actually is, packed with no padding between elements. It is the same low-level
machinery OptionAnyVec is built on (sparse structs
use that one), but for a vec where every slot is present and holds one of two types instead of an optional value
of one type.
A byte-level example¶
Three indices answered as Either<SimpleError, _>: the first fails (SimpleError::BadIndex), the second is a
u8 (7), the third a u16 (0x1234) - the shape of a MultiRead reply where one index is out of range:
use either::Either;
use shrink_wrap::either_any_vec::EitherAnyVecWriter;
use shrink_wrap::prelude::*;
#[derive_shrink_wrap(borrowed, derive(Debug, PartialEq), ww_repr = u8)]
enum SimpleError {
BadIndex,
}
let mut buf = [0u8; 16];
let mut wr = EitherAnyVecWriter::new(&mut buf).unwrap();
wr.write::<SimpleError, ()>(Either::Left(SimpleError::BadIndex)).unwrap();
wr.write::<SimpleError, u8>(Either::Right(7)).unwrap();
wr.write::<SimpleError, u16>(Either::Right(0x1234)).unwrap();
let bytes = wr.finish_and_take().unwrap();
assert_eq!(bytes, [0x60, 0x00, 0x07, 0x34, 0x12, 0x01]);
60: the L/R flag byte, one bit per element MSB-first (011 then padding - Left, Right, Right); 00 the
BadIndex discriminant; 07 the u8; 34 12 the u16 (little-endian); the trailing 01 is the length (in
bytes) of the one value that is Unsized (SimpleError, since it wasn't marked sized) - EitherAnyVec stores
unsized elements' lengths in reverse order at the back, as OptionAnyVec does.
The generated client API¶
Several resources at the same level¶
MultiRead<Out>, MultiCall<Out> and MultiWrite<Out> (wire_weaver_client::MultiRead/MultiCall/MultiWrite)
are implemented for tuples of 2 to 8 of the generated PreparedRead/PreparedCall/PreparedWrite builders, at
any depth - any resources that share a common path prefix and diverge at exactly one element, optionally followed
by one more shared element (e.g. the same property across two array elements):
// N > 2, any depth, not just a root 2-tuple
let (x, y, mode) = (client.read_x(), client.read_y(), client.read_mode())
.multi_read()
.await
.unwrap();
let (wx, wy, wcustom) = (
client.write_x(1),
client.write_y(2),
client.write_custom(custom),
)
.multi_write()
.await
.unwrap();
wx.unwrap();
wy.unwrap();
wcustom.unwrap();
Each item comes back as Result<T, ErrorOwned> in the same order the tuple was given. MultiArgs::Same/
Different - whether every index gets the same serialized argument/value or its own - is chosen automatically by
comparing the serialized bytes of each item.
The same resource can be in a tuple more than once (CLT-49), e.g. two writes over the same I2C bus,
(i2c.i2c_write(a, x, None), i2c.i2c_write(a, y, None)).multi_call(): its id repeats in the request's index list
(List([id, id])), the device handles the indices one after another in that order, and the answers come back in
tuple order.
The same resource across a whole array¶
read_array/call_array/write_array (+ _blocking, wire_weaver_client::multi) take a MultiIndexOwned
selector and the existing generated per-index accessor as a closure, called once per index locally (no network
traffic) just to find the common path:
use wire_weaver_client::multi::{read_array, write_array};
use ww_client_server::MultiIndexOwned;
// write different: one value per channel, in list order
let r = write_array(
MultiIndexOwned::List(vec![UNib32(0), UNib32(1), UNib32(2), UNib32(3)]),
|i| client.ch(i).write_level((i as u8 + 1) * 10),
)
.await
.unwrap();
// read range: same order back
let r = read_array(MultiIndexOwned::Range(UNib32(0)..UNib32(4)), |i| {
client.ch(i).read_level()
})
.await
.unwrap();
Methods returning Result¶
A method returning Result<T, E> (with or without #[errors(..)], CLT-48) goes into a tuple multi_call and into
call_array like any other, mixed freely with methods without Result. Its position answers exactly what its own
call() would, Result<T, CallError<E>>, so moving a call into a batch doesn't change how its result is handled:
Ok(value): the method returnedOk;Err(CallError::Device(e)): the method returnedErr(e), checked against its#[errors(..)]list as in a single call (see Errors);Err(CallError::Unimplemented { .. }): the device's handler returnedUnimplemented;Err(CallError::Client(Error::RemoteError(e))): the index itself failed (BadIndex,ArgsDesFailed, ..).
A method without Result keeps answering Result<T, ErrorOwned>.
// ww_i2c: a write, then a register read of a USB-PD controller, in one USB round trip
let (written, read) = (
i2c.i2c_write(addr, data, None),
i2c.i2c_read(addr, kind, 4, None),
)
.multi_call()
.await?; // the whole request failed: link, timeout, server without multi_req
written?;
let bytes = read?; // CallError<I2cError>: Device(Nack), Unimplemented or Client
// the same fallible method across an array: Ok and Err mixed, one entry per index
let r = call_array(MultiIndexOwned::Range(UNib32(0)..UNib32(4)), |i| {
client.unit(i).halve(i as u8)
})
.await?;
assert!(matches!(r[1], Err(CallError::Device(MethodError::Odd))));
Nothing changes on the device: the server already puts the method's own Result<T, E> into the index's slot, the
same bytes a single call answers with.
The dynamic client¶
DynResource::multi_read/multi_write/multi_call are the same three operations without generated code: child
names the resource inside the array's trait (None for an array of a flat property/method, e.g.
property!(led[]: bool)), and DynMultiArgs/DynMultiValue give Same (one value/argument list for every index)
or Different (one per index, in multi_idx order):
let channel = root.child("channel").unwrap();
channel
.multi_write(
cmd,
MultiIndexOwned::List(vec![UNib32(0), UNib32(1), UNib32(2)]),
Some("gain"),
DynMultiValue::Different(&[num(N::F32(0.1)), num(N::F32(0.2)), num(N::F32(0.3))]),
)
.await
.unwrap();
Errors per index, and for the whole request¶
Most problems land on the one index they belong to and leave the rest answered: an unknown path or index
(BadIndex, BadPath), a property or method the device left out (Unimplemented), one entry of MultiArgs::Different
that doesn't deserialize (ArgsDesFailed), a write whose value doesn't fit its property (PropertyDesFailed). A
deferred method called inside a multi request answers OperationNotSupported in its slot instead of running (see
Limits) - the rest of the request still goes through.
The whole request fails instead (no EitherAnyVec at all) only for something that isn't about one index: the path
is deeper than the depth limit (BadPath), MultiArgs::Different/MultiArgs::Different itself doesn't deserialize
as a list (ArgsDesFailed), or the answer doesn't fit the reply buffer (ResponseSerFailed) - which is also why a
huge Range (e.g. 0..u32::MAX) fails outright rather than running for billions of indices.
Limits¶
- Depth: the request's own path plus the varying element plus an optional
in_each_array_idmust stay withinMAX_DEPTH(16, hardcoded - API-12 would calculate it per API instead); a path one level over fails the whole request withBadPath. - Deferred methods are refused with
OperationNotSupportedinside a multi request, even one that defers although itsmethod_modelsaysimmediate(deferred methods need a reply tied to one request'sseq, which a shared multi-request slot can't carry). - A trait level itself (not a leaf resource) inside a multi request answers
OperationNotSupported, as does a server withoutmulti_req = truefor any multi request at all.