Coming from sbe-tool

Side-by-side mapping for teams migrating from the official Simple Binary Encoding Rust generator (sbe-tool). Within the [published SBE profile] (../design-notes/feature-matrix.md) and docs/SBE_COMPATIBILITY.md, ergo-sbe aims for official-SBE wire fidelity with sbe-tool; the API shape is intentionally different. Do not read this as unqualified “binary compatible with every SBE feature.”

The #1 trap: wrap offset

sbe-tool Rustergo-sbe
wrap argumentBody offset (often 8 for a frame at 0)Message start (often 0)
Where fields livebody_offset + field_offsetmessage_offset + HEADER_LENGTH + field_offset
// Frame at buf[0..]:
// sbe-tool:  enc.wrap(buf, 8)              // body starts at 8
// ergo-sbe:  Enc::wrap(buf, 0)             // message starts at 0
//            Enc::wrap_and_apply_header(buf, 0)

Passing sbe-tool’s 8 into ergo-sbe for a frame at zero mis-aligns every field. Generated rustdoc on wrap / wrap_and_apply_header / decode repeats this callout.

Header modes (fair comparison table)

Use the same logical work on both sides when comparing or porting:

Modeergo-sbesbe-tool
Body onlywrap(buf, 0) + setters — no apply-headerwrap(buf, 8) + setters — no .header(0)
Header + bodywrap_and_apply_header(buf, 0) + setterswrap(buf, 8) then header(0).parent() then setters
Header onlywrap_and_apply_header alonewrap(buf, 8).header(0) alone
  • ergon wrap = message start; sbe-tool wrap = body offset.
  • sbe-tool encoded_length() is body only. ergon encoded_length_with_header() includes the header. Never invent 8 + encoded_length() to “prove” a header was written if .header(0) was not called on the sbe-tool arm.

Full fairness rules for benchmarks: Benchmarks methodology.

Groups: .parent() hopscotch vs closures

sbe-toolergo-sbe
Open group flyweight, fill entries, .parent() backenc.bids(n, |bids| { bids.add(|e| { … })?; Ok(()) })?
Nested groups fight the borrow checkerNested closures end; chain continues in wire order
#![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(b"Urban")
            })?;
            g.add(|mut e| {
                e.speed(60).mpg(25.0);
                e.usage_description(b"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(b"Honda")?
        .model(b"Civic VTi")?
        .activation_code(b"abcdef")?
        .encoded_length_with_header();

    Ok(len)
}
}

Compile-time order: you cannot call asks before bids — the stage type has no asks method. See Wire order via named stages.

Length APIs

Conceptsbe-toolergo-sbe
Body length after encodeencoded_length()body region only via stage encoded_length where exposed
Header + bodycompute yourself (8 + …)encoded_length_with_header() / as_bytes_with_header()
Pre-size bufferoften oversize scratchExact: Encoder::compute_length() / staged *EncodedLength builder; stack [0u8; N] when N is const

Do not default to vec![0u8; 4096] or Vec::with_capacity(MAX) then truncate. See Buffer sizing and Exact sizing.

Decode entry

Both ecosystems typically wrap decoders at the body for direct field access after the header is known. ergon’s entry points take message start (not sbe-tool’s body offset):

Needergo-sbe
Untrusted / networktry_decode / try_wrap / try_fromResult (all failures)
Known-good bufferbare wrap → panic if short; bare decodehybrid (panic if short, Err on wrong template/schema)
Proven-tight hot pathunsafe wrap_unchecked; decode_unchecked = unchecked extent + checked identity

See Trust Boundary.

Version handling

Decoders are version-aware: tail offsets use the wire acting block length, not only the compiled block length. Optional / sinceVersion fields follow schema presence rules. Prefer explicit try_* entry points when reading mixed-version streams.

What has no direct ergon equivalent (and why)

sbe-tool habitergo-sbe
.parent() ownership hopClosures + consuming stage returns
Generic Encoder<State> spellingNamed stage structs + H: HeaderState only for header mode (type-state note)
encoded_length() as full-frame sizeUse *_with_header when you need the frame
Always-on meta / Display noiseOpt-out size knobs: with_display_debug(false), with_meta_attributes(false), with_dispatch(false)

Trust boundary

try_* returns Result on short buffers (and identity mismatches). Bare wrap panics if short. Bare decode is a hybrid: panics if short, but still returns Err on wrong template/schema. decode_unchecked is unchecked extent + checked identity. After a safe constructor succeeds, fixed-field accessors are branch-free. Full detail: Trust Boundary.

Further reading