Flyweight vs Whole-Struct
Use the style that matches how much of the message you touch.
| Style | Best when | Cost | Schema evolution |
|---|---|---|---|
| Flyweight (per-field) | You only read one or a few fields; hot path | Zero-copy; no heap | New fields are optional at call sites (you simply don’t read them) |
*FixedFields + .fixed(...) | You always write the entire fixed block | One struct write, still flyweight buffer | Adding 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-data | Allocates — never use on the hot path. Easier app code for tooling, logging, offline processing | Same 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 DTO | No allocation: groups are iterators, var-data is borrowed | A 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/Stringand 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.