Skip to main content

Module framing

Module framing 

Source
Expand description

I/O-free framing: split length-prefixed frames out of bytes the caller already holds.

The wire format is a 4-byte little-endian payload length followed by that many payload bytes. BlockingFrameReader reads it straight off a std::io::Read; this module is for programs that own their I/O loop instead (io_uring completions, DPDK receive bursts, a non-blocking socket polled by hand) and only need to be told where the frames are in the bytes they have received so far.

Two layers:

  • split_frame / split_frame_limited: stateless and zero-copy. They look at the front of a caller-owned buffer and return the first complete frame as a borrowed slice, None if more bytes are needed, or an error if the length prefix declares more than the limit. The caller keeps its own buffer, cursor and compaction policy, so a receive loop on the hot path pays nothing beyond the bounds checks it would write by hand.
  • FrameDecoder: an owning buffer built on the splitter, for a caller that would rather push received chunks and pull frames out.

The sending side has its counterpart for application requests: the body is encoded in place in the caller’s send buffer, behind room for a header of REQUEST_HEADER_LEN bytes, and seal_request writes the length prefix, the protocol’s tag and the request’s ID in front of it once its length is known (request_body hands out the region to encode into, and frame_request does both around a closure). Frames built at successive offsets lie back to back, ready for one write. A codec crate that needs only framing depends on this crate alone, and a codec whose error is ProtocolError frames with ?. On the receiving side, split_request is the one parser of a request frame’s payload: its ID and its body.

A receive loop over its own buffer:

use melin_wire_protocol::framing::{split_frame, FrameTooLarge};

fn on_receive(buf: &mut Vec<u8>, frames: &mut Vec<Vec<u8>>) -> Result<(), FrameTooLarge> {
    let mut cursor = 0;
    while let Some(frame) = split_frame(&buf[cursor..])? {
        frames.push(frame.payload.to_vec());
        cursor += frame.consumed();
    }
    // Keep the partial frame, if any, for the next receive.
    buf.drain(..cursor);
    Ok(())
}

let mut buf = vec![2, 0, 0, 0, b'h', b'i', 3, 0];
let mut frames = Vec::new();
on_receive(&mut buf, &mut frames)?;
assert_eq!(frames, [b"hi".to_vec()]);
assert_eq!(buf, [3, 0]);

// A prefix declaring more than the limit fails before its payload arrives.
buf.extend_from_slice(&[0xFF, 0xFF]);
assert!(on_receive(&mut buf, &mut frames).is_err());

§Oversized frames

A length above the limit is rejected as soon as the 4 prefix bytes are readable, before any payload arrives. A peer that sends a corrupt or hostile prefix is therefore refused immediately instead of leaving the receiver buffering towards a length it will never accept. Past such a prefix the byte stream has no trustworthy frame boundary left: the connection should be closed, not resynchronised.

§Empty frames

A zero length is a valid frame with an empty payload: it consumes the 4 prefix bytes and nothing else. Framing does not judge content; a protocol that has no use for an empty frame rejects it one layer up, where it already inspects the payload.

Structs§

Frame
A complete frame found at the front of a buffer.
FrameDecoder
An owning frame buffer: push received bytes in, take complete frames out.
FrameTooLarge
A length prefix declared more payload than the limit allows.

Enums§

NotARequest
Why a frame’s payload is not a request: see split_request.
RequestFrameError
A request could not be framed. Nothing was written to the header, so a caller that sends only what a successful call reported sends nothing that could desync the node’s framing.

Constants§

MAX_REQUEST_BODY
The widest request body one frame carries: the tag and the ID count toward MAX_FRAME_SIZE, so a body may take the rest of it.
PREFIX_LEN
Size of the length prefix in front of every frame’s payload.
REQUEST_HEADER_LEN
Bytes written ahead of a request body: the length prefix, the protocol’s one-byte TAG_REQUEST and the request’s eight-byte ID.
REQUEST_TAG_AND_ID_LEN
Bytes of a request frame’s payload ahead of its body: the protocol’s one-byte TAG_REQUEST and the request’s eight-byte ID.

Functions§

frame_request
Frame request id in place at the front of buf, the closure form of request_body and seal_request: encode writes the application’s body into the region request_body returns and reports how many bytes it wrote, then the header goes in front of them. Returns the frame’s length, header included.
request_body
The region of buf a request body is encoded into, ahead of seal_request: everything after the first REQUEST_HEADER_LEN bytes, capped at MAX_REQUEST_BODY bytes, so a codec that sizes its output by the slice it is given never writes a body the frame limit would refuse, even in a send buffer wider than one frame.
request_payload_len
The payload length of a request frame carrying body_len bytes (the tag, the ID and the body), or RequestFrameError::RequestTooLarge if that exceeds MAX_FRAME_SIZE. The one place the request limit is decided, for a sender that writes the header apart from the body as much as for seal_request.
request_tag_and_id
The bytes of a request frame’s payload ahead of its body, for request id: TAG_REQUEST, then the ID, little-endian. The one place they are laid out, for seal_request and for a sender that writes the header apart from the body (melin-client’s blocking connection, which spares itself a copy of the body that way).
seal_request
Seal request id, whose body_len-byte body already sits at buf[REQUEST_HEADER_LEN..]: write the header (the length prefix, the protocol’s tag and the ID) in front of it. Returns the frame’s length, header included: the bytes to send are buf[..n]. The caller never needs the protocol’s tag. The ID is the caller’s to choose: the node echoes it on the request’s final reply frame, and checks nothing about it.
split_frame
Split the first frame off the front of buf, with the protocol’s MAX_FRAME_SIZE as the payload limit.
split_frame_limited
Split the first frame off the front of buf, refusing a payload longer than max_payload bytes.
split_request
Split a request frame’s payload (after the length prefix) into its ID and its body: the one parser of a request, for a node’s readers and for any program that reads requests off the wire (a proxy’s trace, a gateway’s codec). Anything but a TAG_REQUEST payload with room for the ID is NotARequest; the body may be empty.