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

Flyweight vs Whole-Struct

Use the style that matches how much of the message you touch.

StyleBest whenCostSchema evolution
Flyweight (per-field)You only read one or a few fields; hot pathZero-copy; no heapNew fields are optional at call sites (you simply don’t read them)
*FixedFields + .fixed(...)You always write the entire fixed blockOne struct write, still flyweight bufferAdding a required fixed field to the schema → compile error until you set it in the struct
*Domain DTO (.with_domain_objects(DomainVarData::…))Whole message as owned data; enum picks String vs Vec<u8> var-dataAllocates — never use on the hot path. Easier app code for tooling, logging, offline processingSame idea: regenerating after a schema change forces you to fill new struct fields
*Source trait (.with_source_traits(true))Encode your own type, groups and var-data included, without a DTONo allocation: groups are iterators, var-data is borrowedA new field is a new trait method: every implementation stops compiling until it provides it

Encode — whole fixed block as a struct

When you always populate every fixed field, a struct is clearer and schema additions break at compile time:

#![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.)

If the schema later adds a required field to the fixed block, this stops compiling until you add it to the struct literal — you cannot silently omit it.

Decode — individual fields (flyweight)

#![allow(unused)]
fn main() {
// Encode a sample car first (normally bytes come from the wire)
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);
// Now decode — read only the fields you need, no DTO allocation:
let car = CarDecoder::try_from(&buf[..n])?;
assert_eq!(car.serial_number(), 1234);
assert_eq!(car.model_year(), 2013);
assert_eq!(car.code(), Model::A);
assert_eq!(car.engine().capacity(), 2000);
}

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

Decode — whole message as a DTO

Do not use on the latency-sensitive path. DTO decode allocates Vec/String and copies every field. For the hot path, use the flyweight decoder instead.

When you always need (almost) everything, or want to pass a value across threads / into non-SBE code:

#![allow(unused)]
fn main() {
pub fn demo_car_domain_dto(wire: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
    let dec = CarDecoder::try_decode(wire, 0)?;
    // try_from_decoder (not TryFrom/From): two fallible sources — decoder vs
    // try_from_slice_with_header for framed bytes; materialisation can fail.
    let dto = CarDomain::try_from_decoder(dec)?;
    assert_eq!(dto.serial_number, 1234);
    assert!(dto.available); // bool domain field
    assert_eq!(dto.fuel_figures.len(), 2);
    assert_eq!(dto.fuel_figures[0].usage_description, "Urban");
    assert_eq!(dto.manufacturer, "Honda");

    const RE_PAD: usize = 512;
    assert!(wire.len() <= RE_PAD);
    let mut storage = [0u8; RE_PAD];
    let n = dto.encode(&mut storage[..wire.len()])?;
    assert_eq!(&storage[..n], wire, "DTO re-encode must be byte-identical");
    Ok(())
}
}

Encode — your own type through a Source trait

*FixedFields covers the fixed block only. With .with_source_traits(true) each message gets a {Message}Source trait — one getter per field in wire order, impl ExactSizeIterator<Item = impl …EntrySource> per repeating group, &[u8] per var-data — and the encoder gains encode_from(&src) and encoded_length_with_header_from(&src). Implement the trait on the type your application already has; encode_from writes it through the same setters and type-state stages a hand-written encode uses, and returns the terminal stage:

#![allow(unused)]
fn main() {
/// The application's own type: no generated struct is involved.
struct Reminder {
    id: u32,
    text: String,
}

impl NoteSource for Reminder {
    fn note_id(&self) -> u32 {
        self.id
    }
    fn body(&self) -> &[u8] {
        self.text.as_bytes()
    }
}

let reminder = Reminder { id: 9, text: "settle T+1".into() };
let len = NoteEncoder::encoded_length_with_header_from(&reminder)?;
let mut scratch = vec![0u8; len];
let claim = sbe_rt::OfferClaim::new(&mut scratch, |_: &[u8]| -> Result<(), &'static str> { Ok(()) });
NoteEncoder::send(claim, |enc| enc.encode_from(&reminder))?;
}

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

A fixed getter returns what the field’s *FixedFields entry holds (Option<T> for an optional field, whose None writes the null value and whose Some of the null value, NaN for a float, is refused as SomeNullValue), or its domain type when one is configured. A group iterator whose len() does not match what it yields is GroupCountMismatch or GroupFull, as with a hand-written group.

Rule of thumb: one field on the hot path → flyweight. Always fill or always consume the whole message → FixedFields / Domain for clarity and compile-time breakage on schema growth. More on DTOs in Recipes — Domain DTOs.