Encode and Decode
Two styles — pick whichever fits:
fixed() struct (fill every field at once — compile error if a field is missing):
#![allow(unused)]
fn main() {
pub fn demo_fixed_heartbeat() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
// Const length → stack array (no heap).
let mut buf = [0u8; HeartbeatEncoder::compute_length_with_header()];
let nanos: i64 = 1_720_000_000_000_000_000;
// Buffer pre-sized via const compute_length_with_header; try_* still validates extent.
let written = HeartbeatEncoder::try_wrap_and_apply_header(&mut buf, 0)
.unwrap()
.fixed(&HeartbeatFixedFields {
sequence: 7,
timestamp: nanos as u64,
})
.encoded_length_with_header();
let dec = HeartbeatDecoder::try_decode(&buf[..written], 0)?;
assert_eq!(dec.sequence(), 7);
let decoded_ts: DateTime<Utc> = dec.timestamp()?;
assert_eq!(decoded_ts.timestamp_nanos_opt(), Some(nanos));
Ok(buf[..written].to_vec())
}
}
(This code comes from the sbe-feature-tour sample crate.)
{Msg}FixedFields has no Default. Every required scalar must appear in
the struct literal (optional fields use Option only when schema
presence="optional"). That is intentional: zero-filling required IDs hides
bugs. For large messages, build the literal next to the encode call in wire
order.
On a fixed-only message (no groups or var-data), as_bytes_with_header,
as_body_bytes, encoded_length*, and into_remaining_mut exist only after
fixed(&FixedFields). Calling them on the value returned by wrap* is a type
error — that is what stops a reused buffer from publishing leftover body bytes
or packing the next message over a stale body.
Individual setters stay on the unfixed encoder after wrap* and also on
raw_fixed() (body-relative offsets). Prefer fixed(&FixedFields) when you
have every field; use raw_fixed() when you want a dedicated writer:
#![allow(unused)]
fn main() {
// Dedicated raw writer (setters also exist on the unfixed encoder).
// `as_bytes_with_header` is only on FieldsFixed after `fixed(&FixedFields)`.
let mut buf = [0u8; HeartbeatEncoder::compute_length_with_header()];
let mut w = HeartbeatEncoder::try_wrap_and_apply_header(&mut buf, 0)?
.raw_fixed();
w.sequence(7);
w.timestamp_wire(0);
let dec = HeartbeatDecoder::try_from(&buf[..HeartbeatEncoder::ENCODED_LENGTH])?;
assert_eq!(dec.sequence(), 7);
}
(From book/examples/heartbeat-encode.rs — compiled against the feature-tour codec.)
raw_fixed() writes into the buffer you already sized. It does not mark
the message complete: omitted required setters leave stale bytes, and
as_bytes_with_header / encoded_length* / into_remaining_mut stay locked.
Set every required field, then call fixed(&FixedFields) for the
complete-message views. Slicing &buf[..ENCODED_LENGTH] yourself is possible
but is not the completeness-checked path.
Optional fields and apply_nulls
try_wrap_and_apply_header / wrap_and_apply_header write the message header
only — they do not fill optional fixed fields with schema null sentinels
(sbe-tool parity). Unwritten optional bytes retain whatever was already in the
buffer (often zero, sometimes stale).
fixed() closes that gap for you. Optional fields are Option<T> in the
generated FixedFields struct, and fixed() writes the schema null wire image
for every None — including fixed arrays and nested optional composite
members. Since fixed() is the only route to a message’s tails and to
fixed-only complete byte views, the ordinary path never leaves a stale
optional behind:
// `price` is optional; None writes the schema null image, not stale bytes.
// (Illustrative — use your schema's message name and optional field.)
let len = OrderEncoder::wrap_and_apply_header(&mut buf, 0)
.fixed(&OrderFixedFields { symbol: *b"IBM ", price: None })
.encoded_length_with_header();
apply_nulls() remains on the unfixed encoder after wrap*, for the case
where you set individual optional fields yourself and no FixedFields value
describes which optionals are unset. It is generated only for messages that
declare at least one optional field, and it is the counterpart to sbe-tool’s
nullify_optional_fields().
Group entries need no call at all. Every add() writes the null image for
the entry’s optional fields before your setters run, so a reused buffer cannot
leak a previous entry’s value. The cover is narrower than the message-level
path, though: it applies to optional fields with a declared nullValue and a
width of 1-8 bytes. An optional array or composite inside a group entry is
not nulled — set it explicitly if your schema has one.
See Why NullVal Instead of Option and, when porting, Coming from sbe-tool.
Character arrays: fixed-width char fields become [u8; N]. Pass a shorter
&str via the _str setter — auto-padded with NULs. On decode, copy_*
copies the raw bytes into your buffer, or read the slice with vehicle_code():
#![allow(unused)]
fn main() {
let complete_len = CarEncoder::compute_length()
.fuel_figures_ragged(0, |_| Ok(()))?
.performance_figures_ragged(0, |_| Ok(()))?
.manufacturer(5)?
.model(5)?
.activation_code(3)?
.encoded_length_with_header();
const PAD: usize = 256;
assert!(complete_len <= PAD, "car length {complete_len} exceeds pad {PAD}");
let mut storage = [0u8; PAD];
let buf = &mut storage[..complete_len];
let fields = CarFixedFields {
serial_number: 1234,
model_year: 2013,
available: true.into(),
code: Model::A,
some_numbers: [10, 20, 30, 40],
vehicle_code: *b"ABCDEF",
extras: OptionalExtras::default(),
engine: Engine::new(2000, 4, *b"123", 0i8, false.into(), Booster::new(BoostType::TURBO, 210)),
};
let n = CarEncoder::try_wrap_and_apply_header(buf, 0)?
.fixed(&fields)
.fuel_figures(0, |_| Ok(()))?
.performance_figures(0, |_| Ok(()))?
.manufacturer_as_str("Honda")?
.model_as_str("Civic")?
.activation_code_as_str("abc")?
.encoded_length_with_header();
assert_eq!(n, complete_len);
let car = CarDecoder::try_from(&buf[..n])?;
let mut dst = [0u8; 6];
assert_eq!(car.copy_vehicle_code(&mut dst), 6);
assert_eq!(&dst, b"ABCDEF");
assert_eq!(car.vehicle_code(), *b"ABCDEF");
}
(From book/examples/fixed-char-arrays.rs — compiled against the feature-tour codec.)
The Car encoder’s fixed fields include vehicle_code: [u8; 6] (schema char array)
and some_numbers: [u32; 4]. See the feature tour for the
complete Car example with groups and var-data.
Start here for a full runnable map of features:
sbe-feature-tour
(cargo run --manifest-path samples/sbe-feature-tour/Cargo.toml).
More recipes: Recipes.