Typed Message Lifecycle: Zero-Copy Serialization, Framing, and Cancel-Safe I/O#
Serialization (Typed → Logical Wire Format)#
Input: A typed message
M(struct, enum, etc.) which may includePartfields for large binary blobs.Process:
Walk
Mwith a customSerializeimpl.Run
serde_multipart::serialize_bincode(&M), using a custom Serde+bincode serializer.
Emit the “body” as compact bincode into a contiguous
BytesMutbuffer.For each
Part, don’t write anything into the body; just push its backingBytesinto parts in visitation order. The body remains compact bincode without placeholders.
Output: A
Message { body: Bytes, parts: Vec<Bytes> }.
This is the heart of our zero-copy story: large payloads (Part) stay in their original Bytes allocations, and the body just carries lightweight references.
Framing (Logical Message → Transport Frame)#
Input: Message from serialization.
Process:
Wrap with length prefix (
u64, big-endian): total byte length of serialized body + all parts.Build a
Bufthat is multipart:Slice #0: 8-byte length prefix.
Slice #1: body (compact metadata + placeholders).
Slice #2+: each part as-is.
Key Property: Zero-copy –
Bytesis reference-counted, no data copying when building the frameRepresentation: The framed value is a multipart
Buf, not a single contiguous buffer. Internally it’s a small deque ofBytesslices —[len-prefix][body][part₀]…[partₙ]— which implementschunks_vectored(). That meansFrameWritecan hand the OS a vector of slices (writev) without coalescing, preserving zero-copy semantics end-to-end.
Note: “Zero-copy” here refers to user-space: we still incur one unavoidable kernel copy on send (writev) and one on receive (read into BytesMut), but avoid any additional user-space coalescing/copying.
I/O (FrameWrite / FrameReader)#
Send (
FrameWrite):Accepts any
Bufimplementation.Uses
poll_write_vectoredunder the hood:The OS sees multiple
IoSliceS:prefix,body,parts.Calls
writevto push them in one syscall.Benefit: No gather-buffering in userland; fully vectored writes.
Receive (
FrameReader):Reads the length prefix.
Reads exactly that many bytes.
Returns a contiguous
Bytesslice of the frame payload to the caller.
Deframing & Deserialization (Transport → Typed)#
Input: Contiguous frame payload.
Process:
Split payload into body bytes + any parts (zero-copy): this is just slicing a shared buffer into smaller pieces (creating lightweight metadata views, not copying data).
Run custom
Deserialize:Reconstruct
Mby deserializing the compact bincode body and, whenever aPartfield is visited, take the next entry from parts (same visitation order).
Output: A fully-typed message
M.
Highlights#
Zero-copy:
Parts (Bytes) are never copied—only referenced.Vectorized I/O: OS-level
writev(send) and read (recv) avoid extra user-space copiesExtensible: Supports unipart and multipart seamlessly.
Cancel-safe:
FrameRead/FrameWritecan be canceled mid-poll and resumed without corrupting state.
TL;DR#
Send:
serde_multipart::serialize_bincode(M) -> Message → Message::framed() -> impl BufRecv:
FrameReader::next() -> Bytes → Message::from_framed(Bytes) -> Message → serde_multipart::deserialize_bincode::<M>(Message) -> M