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

Method Chaining

ergo-sbe encoders are designed so the entire encode reads as one expression, from wrap_and_apply_header through .fixed(...) and every dynamic tail, ending in .encoded_length_with_header(). Bind only the resulting length; do not retain intermediate encoder variables.

Prefer (one chain, one let):

#![allow(unused)]
fn main() {
let mut buf = [0u8; HeartbeatEncoder::compute_length_with_header()];
// Buffer is exact size from const compute_length_with_header — no bounds check needed.
let len = HeartbeatEncoder::try_wrap_and_apply_header(&mut buf, 0)?
    .fixed(&HeartbeatFixedFields { sequence: 7, timestamp: 0 })
    .encoded_length_with_header();
let dec = HeartbeatDecoder::try_from(&buf[..len])?;
assert_eq!(dec.sequence(), 7);
}

(From book/examples/heartbeat-encode.rs — compiled against the feature-tour codec.)

Staged chaining vs fixed-only

For a fixed-only message like Heartbeat, wrap* returns HeartbeatEncoder<'_, H, FieldsUnfixed>. .fixed(&HeartbeatFixedFields { … }) consumes that value and returns FieldsFixed, which is the only phase that exposes as_bytes_with_header / as_body_bytes / encoded_length* / into_remaining_mut. Individual field setters stay on the unfixed phase and on raw_fixed(); they are not on that complete view.

Avoid (interrupted chain, rebinding):

// Each `let` breaks the chain and splays the pipeline across the screen.
// The `.unwrap()` calls are a code smell — the fallible chain should use `?`.
let enc = CarEncoder::wrap_and_apply_header(&mut buf, 0).fixed(&fields);
let enc = enc.fuel_figures(2, |g| { ... }).unwrap();
let enc = enc.manufacturer_as_str("Honda").unwrap();
let len = enc.encoded_length_with_header();

Every encoder stage is chainable — fixed() and each tail method return the next stage (or Result<NextStage, _>) and compose with ? in the same expression. Intermediate encoder rebinding and manual .unwrap() defeat this design.

For the full Car example with groups and var-data, see the feature tour page.

Decoding

Sequential decode consumes each tail in wire order. For callbacks throughout the chain, start with decoder.ordered():

#![allow(unused)]
fn main() {
pub fn demo_car_ordered_lane(wire: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
    let mut speeds = Vec::new();
    let mut octanes = Vec::new();
    let mut text = Vec::new();

    let len = CarDecoder::try_decode(wire, 0)?
        .ordered()
        .fixed(|car| {
            assert_eq!(car.serial_number(), 1234);
            Ok(())
        })?
        .fuel_figures(|entry, info| {
            // `info` carries the position the wire declares.
            assert_eq!(info.count, 2);
            assert_eq!(info.is_last(), info.index == 1);
            speeds.push(entry.speed());
            Ok(entry.ordered().usage_description(|_| Ok(()))?)
        })?
        .performance_figures(|entry, _info| {
            octanes.push(entry.octane_rating());
            Ok(entry.ordered().acceleration(|a, _| {
                let _ = (a.mph(), a.seconds());
                Ok(())
            })?)
        })?
        .manufacturer_as_str(|s| {
            text.push(s.to_owned());
            Ok(())
        })?
        .model_as_str(|s| {
            text.push(s.to_owned());
            Ok(())
        })?
        .activation_code_as_str(|s| {
            text.push(s.to_owned());
            Ok(())
        })?
        .encoded_length_with_header();

    assert_eq!(speeds, vec![30, 60]);
    assert_eq!(octanes, vec![95]);
    assert_eq!(text, vec!["Honda", "Civic VTi", "abcdef"]);
    assert_eq!(len, wire.len());
    Ok(())
}
}

The fixed callback comes before tails and receives a fixed-fields-only view; each group callback receives the entry and its EntryInfo, and var-data callbacks receive bytes or strictly validated text. Every callback returns Result<_, DecodeError>, so the closures infer. These callbacks borrow the original wire buffer, so bytes and text may be retained as the chain advances. The message chain ends on encoded_length_with_header() / as_bytes_with_header(), like encode — .done() is only to unwrap the staged complete. A nested ordered() walk returns that completion to the parent callback directly. The L3 sample shows this recursive form.

The underlying staged API also supports explicit iteration and skipping. Its group and var-data spellings can require iterator and tuple bindings:

Group entriesinto_<group> gives youWhy
Carry their own groups or var-dataa visit closure, returning the entry’s completionno stride — the completion is where the next entry starts
Fixed-stride (no tails of their own)an iterator, for e in &mut iterthe next entry is offset + block length, so nothing has to be measured

Var-data into_<name>() returns (payload, next). Text fields also offer into_<name>_as_str(), with strict validation and the same tuple shape. These borrowed payloads can be retained while advancing later stages.

Prefer:

#![allow(unused)]
fn main() {
pub fn demo_car_decode_stages(wire: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
    let car = CarDecoder::try_decode(wire, 0)?;
    assert_eq!(car.serial_number(), 1234);
    assert_eq!(car.model_year(), 2013);
    // Domain conversion: BooleanType → bool when configured.
    let available: bool = car.available()?;
    assert!(available);
    assert_eq!(car.code(), Model::A);
    assert_eq!(car.discounted_model(), Model::C); // constant field
    assert_eq!(car.engine().capacity(), 2000);

    // Schema order: groups then var-data, one chain. Both of Car's groups have
    // entries that carry their own tails, so both take a visit closure: the
    // completion the closure returns is where the next entry starts, so the
    // group is walked exactly once.
    let mut speeds = Vec::new();
    let mut octanes = Vec::new();
    let (mfr, decoder) = car
        .into_fuel_figures(|entry| -> Result<_, sbe_rt::DecodeError> {
            speeds.push(entry.speed());
            // usageDescription is this entry's only tail, so reading it
            // completes the entry.
            entry.into_usage_description().map(|(_usage, done)| done)
        })?
        .into_performance_figures(|entry| -> Result<_, sbe_rt::DecodeError> {
            octanes.push(entry.octane_rating());
            // acceleration has fixed-stride entries, so it is a real iterator.
            let mut accel = entry.into_acceleration()?;
            assert_eq!((&mut accel).len(), 2);
            accel.finish()
        })?
        .into_manufacturer_as_str()?;
    assert_eq!(speeds, vec![30, 60]);
    assert_eq!(octanes, vec![95]);
    let (model, decoder) = decoder.into_model_as_str()?;
    let (code, _decoder) = decoder.into_activation_code_as_str()?;
    // All three &str coexist — each borrows 'a from the original wire buffer.
    assert_eq!((mfr, model, code), ("Honda", "Civic VTi", "abcdef"));
    Ok(())
}
}

(Real code from the sbe-feature-tour sample. fuelFigures and performanceFigures entries carry tails, so they take closures; acceleration is fixed-stride, so it is an iterator.)

Consecutive var-data fields chain through the next stage each tuple returns; the borrowed slices stay valid while later stages advance:

#![allow(unused)]
fn main() {
fn count_car_text_bytes(wire: &[u8]) -> Result<usize, sbe_rt::DecodeError> {
    let (manufacturer, next) = CarDecoder::try_from(wire)?
        .skip_fuel_figures()?
        .skip_performance_figures()?
        .into_manufacturer()?;
    let (model, next) = next.into_model()?;
    let (activation_code, _complete) = next.into_activation_code()?;
    Ok(manufacturer.len() + model.len() + activation_code.len())
}
}

For callbacks instead of tuples, use the ordered lane’s <name>(|bytes| ..) and <name>_as_str(|text| ..), which return DecodeError.

The generated staged walk advances once through dynamic tails. Random-access getters used before that walk, including counts or lengths for later tails, can add scans. See Asking before walking.