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

Wire Order via Named Stages

SBE is positional: groups and var-data appear in a fixed schema order with no per-field tags. Two nearly identical groups back-to-back — bids then asks — are common. Swap them and the bytes still look valid; prices land on the wrong side of the book. That mistake is a type error here, not a production incident.

sbe-tool uses .parent() to hand encoder ownership back up the tree. In Rust that fights the borrow checker. ergo-sbe uses scoped closures so nested schemas stay readable:

#![allow(unused)]
fn main() {
pub fn encode_sample_car(buf: &mut [u8]) -> Result<usize, sbe_rt::EncodeError> {
    let mut extras = OptionalExtras::default();
    extras.cruise_control(true).sports_pack(true);

    // Buffer pre-sized from EncodedLength; try_* still validates extent.
    let len = CarEncoder::try_wrap_and_apply_header(buf, 0)
        .unwrap()
        .fixed(&CarFixedFields {
            serial_number: 1234,
            model_year: 2013,
            available: true.into(),
            code: Model::A,
            some_numbers: [10, 20, 30, 40],
            vehicle_code: [b'A', b'B', b'C', b'D', b'E', b'F'],
            extras,
            engine: Engine::new(
                2000,
                4,
                [b'1', b'2', b'3'],
                0i8,
                false.into(),
                Booster::new(BoostType::TURBO, 210),
            ),
        })
        .fuel_figures(2, |g| {
            g.add(|mut e| {
                e.speed(30).mpg(35.9);
                e.usage_description_as_str("Urban")
            })?;
            g.add(|mut e| {
                e.speed(60).mpg(25.0);
                e.usage_description_as_str("Highway")
            })?;
            Ok(())
        })?
        .performance_figures(1, |g| {
            g.add(|mut e| {
                e.octane_rating(95);
                e.acceleration(2, |a| {
                    a.add(|x| {
                        x.mph(30).seconds(4.0);
                        Ok(())
                    })?;
                    a.add(|x| {
                        x.mph(60).seconds(7.5);
                        Ok(())
                    })
                })
            })?;
            Ok(())
        })?
        .manufacturer_as_str("Honda")?
        .model_as_str("Civic VTi")?
        .activation_code_as_str("abcdef")?
        .encoded_length_with_header();

    Ok(len)
}
}

(From sbe-feature-tour — compiles and runs in CI.)

Order is the type-state pattern, implemented as named concrete stage structs (CarAfterFuelFigures), not Encoder<State>. The H: HeaderState generic on every stage is a zero-sized marker for header-present vs body-only — it does not track wire-order. Duplicating the stage graph for HeaderPresent / HeaderAbsent would provide no latency advantage.

pub struct BookEncoder<'a> { /* buf, pos, … */ }
pub struct BookAfterBids<'a> { /* same layout */ }
pub struct BookAfterAsks<'a> { /* same layout */ }

impl BookEncoder<'a> {
    pub fn bids(self, …) -> Result<BookAfterBids<'a>, …> { … }
    // no asks() — bids first on the wire
}
impl BookAfterBids<'a> {
    pub fn asks(self, …) -> Result<BookAfterAsks<'a>, …> { … }
}

After fixed fields you may only call the next group or var-data. BookEncoder has no asks method. Decoders use the same idea (BookDecoder → BookDecoderAfterBids → …).

Group bodies use |g| { g.add(|e| { … }) }: the closure ends, then chaining continues. See Type-state and Benchmarks for the cost claim and the 1.00× gate.