Skip to content

SBI: Sovereign Binary Interface

Your struct’s memory layout IS the wire format.


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.

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.


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 bytes

The preamble carries enough metadata for receivers to:

  1. Validate the frame (magic, version)
  2. Verify type compatibility (schema fingerprint)
  3. Detect cross-architecture frames (arch flags)

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,
}
const sbi = @import("std/sbi");
const reading = Sensor{
.timestamp = 1709251200,
.temperature = 22.5,
.humidity = 0.65,
.station_id = 42,
};
// Encode into a buffer
var buf: [sbi.encodedSize(Sensor)]u8 = undefined;
const frame = try sbi.encode(Sensor, &reading, &buf);
// frame.len == 24 (preamble) + 20 (struct) = 44 bytes
// Zero-copy: returns a pointer INTO the buffer
const decoded = try sbi.decode(Sensor, &buf);
// decoded.temperature == 22.5
// decoded.station_id == 42

No allocations. No parsing. The decoded pointer points directly into buf at byte offset 24.


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.

  • 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 SchemaFingerprintMismatch
const sensor_frame = try sbi.encode(Sensor, &reading, &buf);
const wrong = sbi.decode(Command, &buf);
// error: SchemaFingerprintMismatch

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 layout

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)
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 == 7

If totalPadding returns a non-zero value, you may want to reorder fields to reduce wire overhead.


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-place
var mutable_buf = receive_frame();
const decoded = try sbi.decodeMut(Sensor, &mutable_buf);
// Fields are now in native byte order

The 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.

If your buffer isn’t aligned or you need a detached copy:

// decodeCopy: validates, then copies payload into a stack-allocated struct
const value = try sbi.decodeCopy(Sensor, &buf);
// value is a plain Sensor on the stack — no pointer into buf

SBI supports BLAKE3 Merkle trees over container payloads, enabling field-level verification without decoding the entire frame.

const root = sbi.merkleRoot(payload_bytes);
// root is a [32]u8 — deterministic for the same payload

The payload is split into 32-byte chunks, each BLAKE3-hashed, then pairwise hashed up to a single root.

// Prove that the 'temperature' field (offset 8, size 4) is part of the container
const proof = try sbi.prove(
allocator,
payload_bytes,
8, // field offset
4, // field size
);
defer proof.deinit(allocator);
const valid = sbi.verify(proof);
// valid == true if the chunk + siblings hash to the expected root

This 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

All SBI operations return explicit errors — no panics, no exceptions:

ErrorCause
InvalidMagicFirst 4 bytes are not "SBI\x00"
UnsupportedVersionWire format version not recognized
SchemaFingerprintMismatchType layout doesn’t match the frame
BufferTooSmallBuffer can’t hold preamble + struct
ArchitectureMismatchSender’s arch differs (use decodeMut)
InvalidAlignmentBuffer 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,
};

  • 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)
  • 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)

FunctionPurpose
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

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