SBI: Sovereign Binary Interface
Sovereign Binary Interface (SBI)
Section titled “Sovereign Binary Interface (SBI)”Your struct’s memory layout IS the wire format.
What is SBI?
Section titled “What is SBI?”SBI is Janus’ native binary serialization format. Unlike Protocol Buffers, MessagePack, or JSON, SBI performs zero encoding — the bytes in memory are the bytes on the wire. Decoding on the same architecture is a pointer cast, not deserialization.
The trade-off is explicit: SBI works with fixed-size extern struct types only. No variable-length strings, no optional fields, no schema evolution. What you declare is what you get — honest, predictable, fast.
The Core Invariant
Section titled “The Core Invariant”wire bytes == memory bytes (same architecture)A struct encoded by one peer can be decoded by another peer on the same architecture with zero copies — the receiver casts a pointer into the buffer and reads fields directly.
Wire Format
Section titled “Wire Format”Every SBI frame consists of a 24-byte preamble followed by the raw struct bytes:
Offset Size Field────── ──── ──────────────────────────────────[0..4) 4 Magic: "SBI\x00"[4] 1 Version: 0x01[5] 1 ArchFlags (endianness, pointer width, alignment)[6..8) 2 CapFlags (pure, deterministic, trusted)[8..24) 16 Schema fingerprint (truncated BLAKE3)[24..) var Payload: raw extern struct bytesThe preamble carries enough metadata for receivers to:
- Validate the frame (magic, version)
- Verify type compatibility (schema fingerprint)
- Detect cross-architecture frames (arch flags)
Quick Start
Section titled “Quick Start”Define Your Type
Section titled “Define Your Type”SBI works with extern struct — types with a guaranteed, platform-defined memory layout:
const Sensor = extern struct { timestamp: u64, temperature: f32, humidity: f32, station_id: u32,}Encode
Section titled “Encode”const sbi = @import("std/sbi");
const reading = Sensor{ .timestamp = 1709251200, .temperature = 22.5, .humidity = 0.65, .station_id = 42,};
// Encode into a buffervar buf: [sbi.encodedSize(Sensor)]u8 = undefined;const frame = try sbi.encode(Sensor, &reading, &buf);// frame.len == 24 (preamble) + 20 (struct) = 44 bytesDecode (Zero-Copy)
Section titled “Decode (Zero-Copy)”// Zero-copy: returns a pointer INTO the bufferconst decoded = try sbi.decode(Sensor, &buf);// decoded.temperature == 22.5// decoded.station_id == 42No allocations. No parsing. The decoded pointer points directly into buf at byte offset 24.
Schema Fingerprinting
Section titled “Schema Fingerprinting”SBI prevents accidental deserialization of wrong types using BLAKE3-based schema fingerprints.
At compile time, Janus generates a Canonical Layout Descriptor (CLD) — a deterministic string encoding of the struct’s field names, types, offsets, and sizes:
sbi:struct{timestamp:u64@0:8,temperature:f32@8:4,humidity:f32@12:4,station_id:u32@16:4}This CLD is BLAKE3-hashed and truncated to 16 bytes. The fingerprint goes into the preamble.
What This Means
Section titled “What This Means”- Same struct layout = same fingerprint, regardless of struct name
- Different field name = different fingerprint
- Different field type = different fingerprint
- Different field order = different fingerprint (different offsets)
If you encode a Sensor and try to decode it as a Command, the fingerprint check fails immediately. No silent corruption.
// This fails at decode time with SchemaFingerprintMismatchconst sensor_frame = try sbi.encode(Sensor, &reading, &buf);const wrong = sbi.decode(Command, &buf);// error: SchemaFingerprintMismatchType CID
Section titled “Type CID”For content-addressed registries, SBI provides a full 32-byte Type Content ID — the complete BLAKE3 hash of the CLD:
const cid = comptime sbi.typeCid(Sensor);// cid is a [32]u8 — unique identifier for this struct's wire layoutLayout Analysis
Section titled “Layout Analysis”SBI provides comptime tools to inspect struct layout — useful for optimization and debugging:
const layout = comptime sbi.fieldLayout(Sensor);// layout[0] = { .name = "timestamp", .offset = 0, .size = 8, .padding_before = 0 }// layout[1] = { .name = "temperature", .offset = 8, .size = 4, .padding_before = 0 }// ...
const padding = comptime sbi.totalPadding(Sensor);// padding == 0 (well-packed struct)
const is_extern = comptime sbi.isExternLayout(Sensor);// is_extern == true (wire = memory identity holds)Padding Detection
Section titled “Padding Detection”const Wasteful = extern struct { flag: bool, // 1 byte at offset 0 // 7 bytes padding here! value: u64, // 8 bytes at offset 8};
const pad = comptime sbi.totalPadding(Wasteful);// pad == 7If totalPadding returns a non-zero value, you may want to reorder fields to reduce wire overhead.
Cross-Architecture Decoding
Section titled “Cross-Architecture Decoding”When sender and receiver have different architectures (e.g., little-endian sender, big-endian receiver), the standard decode returns ArchitectureMismatch. Use decodeMut for in-place fixup:
// decodeMut: validates, then byte-swaps multi-byte fields in-placevar mutable_buf = receive_frame();const decoded = try sbi.decodeMut(Sensor, &mutable_buf);// Fields are now in native byte orderThe fixup is generated at comptime from the struct’s type info — only multi-byte integer and float fields are swapped. Single-byte fields and byte arrays pass through unchanged.
Copy Decode
Section titled “Copy Decode”If your buffer isn’t aligned or you need a detached copy:
// decodeCopy: validates, then copies payload into a stack-allocated structconst value = try sbi.decodeCopy(Sensor, &buf);// value is a plain Sensor on the stack — no pointer into bufMerkle Proofs
Section titled “Merkle Proofs”SBI supports BLAKE3 Merkle trees over container payloads, enabling field-level verification without decoding the entire frame.
Computing the Root
Section titled “Computing the Root”const root = sbi.merkleRoot(payload_bytes);// root is a [32]u8 — deterministic for the same payloadThe payload is split into 32-byte chunks, each BLAKE3-hashed, then pairwise hashed up to a single root.
Generating a Proof
Section titled “Generating a Proof”// Prove that the 'temperature' field (offset 8, size 4) is part of the containerconst proof = try sbi.prove( allocator, payload_bytes, 8, // field offset 4, // field size);defer proof.deinit(allocator);Verifying a Proof
Section titled “Verifying a Proof”const valid = sbi.verify(proof);// valid == true if the chunk + siblings hash to the expected rootThis is useful for:
- Selective field disclosure: Prove one field’s value without revealing the rest
- Tamper detection: Verify payload integrity without a full decode
- Distributed verification: Send proof + field value to a verifier
Error Handling
Section titled “Error Handling”All SBI operations return explicit errors — no panics, no exceptions:
| Error | Cause |
|---|---|
InvalidMagic | First 4 bytes are not "SBI\x00" |
UnsupportedVersion | Wire format version not recognized |
SchemaFingerprintMismatch | Type layout doesn’t match the frame |
BufferTooSmall | Buffer can’t hold preamble + struct |
ArchitectureMismatch | Sender’s arch differs (use decodeMut) |
InvalidAlignment | Buffer not aligned for zero-copy cast |
const result = sbi.decode(Sensor, &buf) catch |err| switch (err) { error.SchemaFingerprintMismatch => { log.warn("incompatible sensor firmware version"); return default_reading; }, error.ArchitectureMismatch => { // Fall back to copy decode with fixup return try sbi.decodeCopy(Sensor, &buf); }, else => return err,};When to Use SBI
Section titled “When to Use SBI”Use SBI When
Section titled “Use SBI When”- Peers run the same binary or same struct definitions
- You need zero-copy performance (sensor telemetry, IPC, shared memory)
- Payloads are fixed-size (no strings, no optional fields)
- You want cryptographic type identity (fingerprints, Merkle proofs)
Don’t Use SBI When
Section titled “Don’t Use SBI When”- You need schema evolution (adding/removing fields over time)
- Payloads contain variable-length data (strings, arrays)
- You need human-readable wire formats
- Peers run different struct versions (use versioned protocols instead)
API Summary
Section titled “API Summary”| Function | Purpose |
|---|---|
encode(T, value, buf) | Encode struct into SBI frame |
encodedSize(T) | Total frame size (preamble + struct) |
decode(T, buf) | Zero-copy decode (same arch only) |
decodeCopy(T, buf) | Copy decode (any alignment) |
decodeMut(T, buf) | Decode with endian fixup (cross arch) |
schemaFingerprint(T) | 16-byte BLAKE3 fingerprint |
typeCid(T) | 32-byte content ID |
fieldLayout(T) | Comptime field layout table |
totalPadding(T) | Sum of padding bytes |
merkleRoot(data) | BLAKE3 Merkle root |
prove(alloc, data, offset, size) | Generate inclusion proof |
verify(proof) | Verify inclusion proof |
Profile
Section titled “Profile”SBI is a :core profile module. The current implementation covers fixed-size containers only. Variable-size containers (strings, dynamic arrays) are planned for the :service profile.
“The fastest serialization is no serialization.” — SBI Doctrine