What Generated Code Looks Like

For a schema with one message (Car), ergo-sbe emits a single Rust module with:

Decoder (flyweight over &[u8])

// Each message gets a zero-allocation decoder.
pub struct CarDecoder<'a> {
    pub(crate) buf: &'a [u8],
    pub(crate) offset: usize,
    pub(crate) acting_version: u16,
    pub(crate) acting_block_length: usize,
}

impl<'a> CarDecoder<'a> {
    // Illustrative only — real values from your schema.
    pub const SCHEMA_ID: u16 = 1;
    pub const TEMPLATE_ID: u16 = 1;
    pub const BLOCK_LENGTH: usize = 45;
    pub const HEADER_LENGTH: usize = 8;

    // Checked framed entry (message start). Validates header + fixed extent.
    pub fn try_decode(buf: &'a [u8], pos: usize)
        -> Result<Self, sbe_rt::DecodeError> { ... }

    // Proves the header+fixed-body extent and panics if short.
    // Caller does not need `unsafe` — the proof is in the constructor.
    pub fn wrap(buf: &'a [u8], message_offset: usize,
               acting_block_length: usize, acting_version: u16)
        -> Self { ... }

    // Full dynamic-tail structural check (associated, not `car.verify()`).
    pub fn verify(buf: &[u8]) -> Result<(), sbe_rt::VerifyError> { ... }

    // Fixed fields are random-access — zero-copy reads after a checked wrap.
    #[inline]
    pub fn serial_number(&self) -> u64 {
        let offset = self.offset + 0;
        u64::from_le_bytes(/* private read after extent proof */)
    }
}

Encoder (type-state stages)

// Wire order is enforced by named stage types. The root encoder also carries a
// fields-state parameter: tails are reachable only after `fixed()`.
pub struct CarEncoder<
    'a,
    H: sbe_rt::HeaderState = sbe_rt::HeaderPresent,
    F: sbe_rt::FieldsState = sbe_rt::FieldsUnfixed,
> { ... }
pub struct CarAfterFuelFigures<'a, H: sbe_rt::HeaderState = sbe_rt::HeaderPresent> { ... }
pub struct CarAfterPerformanceFigures<'a, H: sbe_rt::HeaderState = sbe_rt::HeaderPresent> { ... }
pub struct CarComplete<'a, H: sbe_rt::HeaderState = sbe_rt::HeaderPresent> { ... }

// `fixed()` moves FieldsUnfixed -> FieldsFixed; the tail methods exist only on
// the fixed phase, so `wrap(...).fuel_figures(...)` is a compile error.
pub type CarUnfixedEncoder<'a, H = sbe_rt::HeaderPresent> =
    CarEncoder<'a, H, sbe_rt::FieldsUnfixed>;

impl<'a, H: sbe_rt::HeaderState> CarEncoder<'a, H, sbe_rt::FieldsUnfixed> {
    pub fn fixed(self, fields: &CarFixedFields) -> CarEncoder<'a, H, sbe_rt::FieldsFixed> { ... }
}

// Calling stages out of order is a type error — `CarEncoder` has no `asks()`.
impl<'a, H: sbe_rt::HeaderState> CarEncoder<'a, H, sbe_rt::FieldsFixed> {
    pub fn fuel_figures(self, count: u16, f: impl FnOnce(...) -> ...) -> Result<CarAfterFuelFigures> { ... }
}
impl<'a> CarAfterFuelFigures<'a> {
    pub fn performance_figures(self, ...) -> Result<CarAfterPerformanceFigures> { ... }
}
impl<'a> CarComplete<'a> {
    pub fn encoded_length_with_header(&self) -> usize { ... }
    pub fn as_bytes_with_header(&self) -> &[u8] { ... }
}

Metadata: no field-name collisions

Utility methods like remaining, buffer, as_bytes_with_header, and as_body_bytes are scoped inside a zero-copy metadata struct returned by get_metadata(). This means a schema field named remaining or buffer generates dec.remaining() / dec.buffer() as field accessors — no _field suffix needed. No generated method name can ever collide with a user's schema field name.

What does remaining() mean?

Receiverremaining() means
Group decoder (e.g. FuelFiguresDecoder)Entry count left (usize) — not bytes
dec.get_metadata() / enc.get_metadata()Byte slice after the acting fixed block (&[u8])
Schema field named remainingOrdinary field accessor (natural name, no rename)

Session framing (header then app payload) must use get_metadata().remaining() for the payload bytes.

let dec = CarDecoder::try_decode(&buf, 0)?;
dec.serial_number();                              // field accessor — never collides
dec.get_metadata().remaining();                   // metadata — never collides
dec.get_metadata().buffer();                      // metadata — never collides
// Car has groups/var-data: metadata is fixed-block only (not a full frame).
dec.get_metadata().as_fixed_region_with_header()?;
// Complete frame after walking tails, or rescan without consuming:
// complete.as_bytes_with_header() / dec.as_bytes_with_header()?
dec.encoded_length_with_header()?;                // hot path stays on base struct

The metadata struct holds a reference to the parent (zero-copy):

pub struct CarDecoderMetadata<'m, 'a> {
    decoder: &'m CarDecoder<'a>,
}

Encoders have the same pattern. Fixed-only messages expose as_body_bytes / as_bytes_with_header only after fixed(&FixedFields) (on the encoder and on FieldsFixed metadata). Messages with tails use as_fixed_body_bytes / as_fixed_region_with_header until the complete stage.

let meta = enc.get_metadata();
meta.as_fixed_body_bytes();            // fixed block only when message has tails
meta.as_fixed_region_with_header();    // header + fixed block — not full frame
meta.message_offset();                 // message start in buffer

Metadata limits (tailed messages)

APISpanFull Car frame?
meta.limit()body start + acting block lengthNo — stops at fixed block end
meta.as_fixed_region_with_header()?header + acting fixed blockNo
meta.remaining()bytes after that fixed endMay still include unread tails of this message
complete stage as_bytes_with_header()header through last var-dataYes (after walking tails)
dec.as_bytes_with_header()? (inherent rescan)same full frame without consuming stagesYes (rescans tails)

Do not use meta.limit() as the next-message offset in a multi-message buffer for Car-shaped schemas — that truncates at the fixed block.

acting_version / acting_block_length (dual surface)

These remain inherent on the message decoder for the hot path (dec.acting_version(), dec.acting_block_length()) and are also exposed on metadata (dec.get_metadata().acting_version()) for a uniform placement facet. Both paths return the same values. They are reserved method names: a schema field named actingVersion becomes acting_version_field on the decoder.

Exact buffer sizing

// Fixed-only messages: const length.
let mut buf = [0u8; HeartbeatEncoder::compute_length_with_header()];

// Variable-length: staged builder (zero allocation).
let len = CarEncoder::compute_length()
    .fuel_figures_ragged(2, |ff| {
        ff.add()?.usage_description(5)?;
        Ok(())
    })?
    .performance_figures_ragged(0, |_| Ok(()))?
    .manufacturer(5)?
    .model(9)?
    .activation_code(6)?
    .encoded_length_with_header();

Configuration controls output size

Every aspect of generated output is configurable:

GenerationConfig::new("msgs")
    .with_display_debug(false)   // omit Debug/Display impls
    .with_meta_attributes(false) // omit *_ENCODING_OFFSET etc.
    .with_dispatch(false)        // omit AnyMessage/FrameCursor
    .with_domain_objects(DomainVarData::Bytes) // owned DTOs

A default single-message schema with these knobs off produces minimal output.

GenerationConfig::new enables display/debug, metadata, and dispatch, and disables domain objects, deprecated attributes, blanket enum options, and automatic bool mapping. GenerationProfile::Lean turns off the three output-heavy conveniences (display/debug, metadata, dispatch). See the canonical option table.

Real example

The sbe-feature-tour sample builds and exercises a real Car schema. Run cargo test inside that directory to see the generated API in action, or open src/generated/feature_tour.rs after cargo build.