Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.