Protocol Context
brec separates packet structure from payload runtime state.
Blocks are context-free. Payloads may require extra runtime data during:
- encoding
- decoding
- size calculation
- packet read/write operations
This runtime data is called ProtocolContext.
Why It Exists
Some payloads cannot be encoded or decoded from bytes alone.
Typical examples:
- encryption settings
- decryption settings
- external lookup state
- user-defined runtime options
For the built-in crypto integration that uses this mechanism, see Crypt.
Instead of passing an arbitrary generic options type through the whole API, brec binds a context type and protocol size limits to the generated protocol through ProtocolSchema.
Core Idea
Each payload defines:
pub trait ProtocolSchema {
type Context<'a>;
const MAX_PAYLOAD_LEN: u32;
const MAX_PACKET_LEN: u64;
const INITIAL_PACKET_BUFFER_CAPACITY: usize;
}
All payload-specific traits then receive this context explicitly:
fn encode(&self, ctx: &mut Self::Context<'_>) -> std::io::Result<Vec<u8>>;
fn decode(buf: &[u8], ctx: &mut Self::Context<'_>) -> std::io::Result<Self>;
fn size(&self, ctx: &mut Self::Context<'_>) -> std::io::Result<u64>;
As a result:
- block logic stays clean
- payload logic can use runtime state when needed
- packet and storage APIs stay explicit about where context is consumed
- packet and payload readers share the same configured size limits
MAX_PAYLOAD_LEN is the maximum accepted payload body length. MAX_PACKET_LEN is the maximum accepted packet body length, excluding PacketHeader. INITIAL_PACKET_BUFFER_CAPACITY is the initial allocation used by PacketBufReader; it is a performance hint, not a validity boundary.
For generated protocols these values are configured through brec::generate!(). See Code Generation.
Default Context
If a payload does not need any runtime state, use the default context:
type Context<'a> = brec::DefaultProtocolContext;
DefaultProtocolContext is just ().
Generated Context
When you use brec::generate!(), the macro generates a crate-local ProtocolContext<'a>.
If there are no custom context entries, it becomes:
pub type ProtocolContext<'a> = ();
If custom context types exist, generate!() builds an enum:
pub enum ProtocolContext<'a> {
None,
MyOptions(&'a mut MyOptions),
}
The working examples are available in the examples directory of the repository.
Declaring a Custom Context Type
Mark a type with #[context]:
use brec::payload;
#[context]
pub struct MyOptions {
pub prefix: String,
}
This type is not treated as a regular payload. It is collected only to build ProtocolContext<'a>.
In other words, #[context] means:
- do not generate a normal payload variant for this type
- do generate a matching
ProtocolContextenum variant - pass this value by mutable reference during payload operations
Manual Payload Implementation with Context
If you implement payload traits manually, you can use the generated context and extract your runtime state.
Important: #[payload] already generates part of the boilerplate for the type itself.
In the common manual case you only implement the payload-specific logic such as:
PayloadEncodePayloadEncodeReferredPayloadDecode<T>PayloadSize- optionally
PayloadCrc
You do not need to manually reimplement the basic glue that #[payload] already provides for the example shape below.
Example:
use brec::{PayloadCrc, PayloadDecode, PayloadEncode, PayloadEncodeReferred, PayloadSize};
#[context]
pub struct MyOptions {
pub prefix: String,
}
impl MyOptions {
fn extract_prefix<'a>(ctx: &'a mut crate::ProtocolContext<'_>) -> std::io::Result<&'a str> {
match ctx {
crate::ProtocolContext::MyOptions(options) => Ok(options.prefix.as_str()),
crate::ProtocolContext::None => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"MyPayload expects ProtocolContext::MyOptions",
)),
}
}
}
#[payload]
pub struct MyPayload {
pub value: String,
}
impl PayloadEncode for MyPayload {
fn encode(&self, ctx: &mut Self::Context<'_>) -> std::io::Result<Vec<u8>> {
let prefix = MyOptions::extract_prefix(ctx)?;
Ok(format!("{}{}", prefix, self.value).into_bytes())
}
}
impl PayloadEncodeReferred for MyPayload {
fn encode(&self, _ctx: &mut Self::Context<'_>) -> std::io::Result<Option<&[u8]>> {
Ok(None)
}
}
impl PayloadDecode<MyPayload> for MyPayload {
fn decode(buf: &[u8], ctx: &mut Self::Context<'_>) -> std::io::Result<MyPayload> {
let prefix = MyOptions::extract_prefix(ctx)?.to_owned();
let value = String::from_utf8(buf.to_vec())
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
let value = value
.strip_prefix(&prefix)
.unwrap_or(&value)
.to_owned();
Ok(MyPayload { value })
}
}
impl PayloadSize for MyPayload {
fn size(&self, ctx: &mut Self::Context<'_>) -> std::io::Result<u64> {
Ok(PayloadEncode::encode(self, ctx)?.len() as u64)
}
}
impl PayloadCrc for MyPayload {}
This is the same pattern used in the real example. The full example can be found in the repository under examples/ctx.
Passing Context into Readers and Writers
Context is supplied at the operation boundary.
Examples:
let packet = Packet::new(
vec![Block::MyBlock(MyBlock { id: 1 })],
Some(Payload::MyPayload(MyPayload {
value: "hello".to_owned(),
})),
);
let mut ctx = ProtocolContext::MyOptions(&mut options);
writer.insert(packet, &mut ctx)?;
let mut ctx = ProtocolContext::MyOptions(&mut options);
for packet in reader.iter(&mut ctx) {
let packet = packet?;
// ...
}
let mut ctx = ProtocolContext::MyOptions(&mut options);
match packet_reader.read(&mut ctx)? {
NextPacket::Found(packet) => {
let _ = packet;
}
_ => {}
}
The key idea is:
- payload type owns the serialization logic
- context is created by the caller
- reader and writer APIs receive that context explicitly per operation
Practical Rule
Use context only for runtime state that genuinely belongs to payload processing.
Good candidates:
- crypto options
- decode-time lookup state
- encode/decode feature flags
Bad candidates:
- block-level concerns
- global application state unrelated to payload bytes
- values that should be stored inside the payload itself