Multi-Schema Patterns
SBE schemas often share types (messageHeader, groupSizeEncoding,
composites, enums, sets). ergo-sbe supports two approaches:
| Approach | When | Method |
|---|---|---|
xi:include (standard) | Schema files live together; official SBE portability matters | <include href="common-types.xml"/> — parse_file resolves includes relative to the base dir |
Shared Ir (programmatic) | Schemas are parsed from strings, generated, or live in separate repos; no filesystem dependency | parse_with_shared / parse_file_with_shared — seed one parse from another’s resolved types |
The <include> path is what the SBE spec expects. The shared-Ir path is a
convenience for tooling, build scripts, and any workflow where you already have
the shared schema parsed in memory.
Shared Ir — parse, then share
// 1. Parse the shared schema once (composites, enums, sets).
let common = ergo_sbe::parse_file("schemas/common-types.xml")?;
// 2. Parse a consumer schema — no <include> needed.
let orders = ergo_sbe::parse_file_with_shared("schemas/orders.xml", &common)?;
// 3. Each schema gets its own module.
let generator = ergo_sbe::Generator::new(
ergo_sbe::GenerationConfig::new("common_types").with_shared_module("common_types"),
);
let modules = generator.generate_multi(&[
(&ergo_sbe::Schema::from_ir(common), "common_types"),
(&ergo_sbe::Schema::from_ir(orders), "orders"),
])?;
With with_shared_module("common_types"), the first entry owns the shared
enums/sets/composites; later entries pub use super::common_types::* and skip
duplicate type generation.
Module names must be unique, non-empty Rust identifiers (no path separators or
keywords). Before any file is written, shared types with the same name are
compared by a canonical wire fingerprint (token order, primitive encodings,
offsets, presence, null/min/max, discriminants/choices, sinceVersion, and
schema byte order). A name collision with a different fingerprint fails
generation with GenerateError::IncompatibleSharedType rather than silently
reusing the first schema’s layout.
parse_with_shared from in-memory strings
let common = ergo_sbe::parse(
r#"<?xml version="1.0"?>
<messageSchema package="common" id="0" version="1" byteOrder="littleEndian">
<types>
<composite name="messageHeader">
<type name="blockLength" primitiveType="uint16"/>
<type name="templateId" primitiveType="uint16"/>
<type name="schemaId" primitiveType="uint16"/>
<type name="version" primitiveType="uint16"/>
</composite>
<composite name="Price">
<type name="mantissa" primitiveType="int64"/>
<type name="exponent" primitiveType="int8"/>
</composite>
</types>
</messageSchema>"#,
)?;
// No <types> / <include> — Price resolves from `common`.
let orders = ergo_sbe::parse_with_shared(
r#"<?xml version="1.0"?>
<messageSchema package="orders" id="1" version="1" byteOrder="littleEndian"
headerType="messageHeader">
<message name="NewOrder" id="1">
<field name="price" id="1" type="Price"/>
</message>
</messageSchema>"#,
&common,
)?;
The shared Ir path does not recover bare top-level <type> typedefs
(those are inlined during parsing and dropped from the token stream). Reference
them through a <composite> / <enum> / <set> in the shared schema instead.
Full build.rs — one helper call
Module names are supplied, not derived from file stems, so a hyphenated
common-types.xml can emit common_types.rs. The helper parses the shared
schema first, resolves consumers against it, validates the whole set, then
writes. A late consumer failure leaves no partial files. Cargo watches every
root and every resolved include.
// build.rs
use std::path::Path;
use ergo_sbe::{GenerationConfig, SchemaFile, generate_multi_to_out_dir};
fn main() -> ergo_sbe::miette::Result<()> {
generate_multi_to_out_dir(
SchemaFile::new(Path::new("schemas/common-types.xml"), "common_types"),
&[
SchemaFile::new(Path::new("schemas/orders.xml"), "orders"),
SchemaFile::new(Path::new("schemas/fills.xml"), "fills"),
],
GenerationConfig::new("common_types"),
)?;
Ok(())
}
Consumer modules import the shared module for cross-schema type resolution:
mod common_types { include!(concat!(env!("OUT_DIR"), "/common_types.rs")); }
mod orders {
use super::common_types::*; // shared types + header composite
include!(concat!(env!("OUT_DIR"), "/orders.rs"));
}
mod fills {
use super::common_types::*;
include!(concat!(env!("OUT_DIR"), "/fills.rs"));
}
AnyMessage is generated once per schema module. Template ids are not unique
across schemas. Generate an opt-in AnySchemaMessage over every configured
schema with Generator::generate_schema_dispatch:
#![allow(unused)]
fn main() {
use ergo_sbe::{GenerateError, GeneratedModule, GenerationConfig, Generator, Schema};
fn dispatcher(market: &Schema, trading: &Schema) -> Result<GeneratedModule, GenerateError> {
Generator::new(GenerationConfig::new("market")).generate_schema_dispatch(
&[(market, "market"), (trading, "trading")],
"any_schema",
)
}
}
Write the returned source to its path beside the codec modules, then include
any_schema.rs in a sibling module. This also works when the codecs were
generated separately with different conversion settings. Each codec must have
AnyMessage dispatch enabled. The combined enum wraps it in a variant named
from the module, such as Market and Trading, and reads schemaId before
templateId. Decode errors retain the originating schema’s error type.
Schema ids and module names must be unique. Headers must declare the same wire
layout and byte order, with a required, encoded schemaId. Custom layouts and
big-endian headers work; mixed layouts or byte orders are rejected as ambiguous.
Single-schema generation and the ordinary codec hot paths are unchanged.
Pass a slice bounded to one transport frame to AnySchemaMessage::decode.
An unknown schema id returns Other { schema_id, template_id, frame }. An
unknown template in a known schema remains that module’s AnyMessage::Unknown.
Both retain the supplied header and body. as_bytes() borrows the complete
frame without allocating, including future-version fields and unknown frames,
for persistence or forwarding.
See also: sbe-codegen-examples (reusable generator setup), multi_schema_versioning_test (versioned schemas with shared types), exchange-example (multi-schema exchange feed with IPC).