Graven

zerocopy-idl — Design

Historical document. Written on 2026-11-12, when the project was called zidl (later Graven). It records the original intent and decisions; several were refined later. The normative text is the format specification.

Date: 2026-11-12 · Status: draft for approval

1. Intent

A general-purpose IDL (games, banking, streaming, IoT) for describing messages that travel over the network and are written to disk. The motivation: FlatBuffers, Cap'n Proto, Protobuf and FBE have poor trade-offs for anyone who wants to build the message front to back, straight into a BytesMut taken from a pool (the bytes crate). The FlatBuffers builder writes back to front and is painful to use.

The goal of the project is to research and test until we reach a format that is "close to perfect", measured by beating FlatBuffers and Cap'n Proto in benchmarks.

2. Priorities (the user's, in this order)

  1. Schema evolution — producer and consumer on different versions coexist without a coordinated deploy.
  2. Zero-copy reads — fields read straight from the buffer, without deserializing.
  3. Size — bytes on the wire and on disk.

Hard constraint: forward-only writing into a pooled BytesMut.

3. Goals, non-goals, anti-goals

Goals

Non-goals (for now)

Anti-goals (a failure even if it "works")

4. Binary format

Every decision in this section is the assistant's choice, accepted by the user in the conversation, to be validated by benchmark.

4.1 Conventions

4.2 Table (the evolvable type)

[u16 fixed_len][fixed part: fixed_len bytes]

Slot kinds in the fixed part:

Type Slot
u8..u128, i8..i128, f32, f64 N bytes
bool 2 bits (presence + value) in a bit byte (§4.3)
enum size of the declared base type (u8/u16/u32)
struct (frozen) inline, fixed size
string, bytes, [T], nested table i32 offset
union u16 tag + i32 offset (tag 0 = none)

4.3 Presence on the wire ("0 is a value too")

Absence must be on the wire, not only in the schema: the wire does not carry which fields were deprecated, and there is no guarantee every reader was updated (old files on disk, IoT devices, lagging shards).

4.3.1 Required vs optional = the reader's expectation

4.4 Variable-size data

4.5 Root

The buffer starts directly at the root table ([u16 fixed_len]...). No magic number in the core; framing (size, type, version) is the job of an optional layer outside the initial scope.

4.6 Enums, unions and structs

4.7 Read safety

5. Writing

5.1 Pool

pub trait BufPool { fn take(&self) -> BytesMut; fn give(&self, b: BytesMut); }

The runtime provides a simple implementation (e.g. Mutex<Vec<BytesMut>>, or thread-local); details are left to the builder. finish() returns Bytes (freeze) ready to send without a copy.

5.2 Model

5.3 Lists

6. Schema language (.zidl)

Sketch (the final syntax is the builder's decision, keeping the concepts):

namespace bank.v1;

enum Currency : u16 { BRL = 0, USD = 1, EUR = 2 }

struct Vec3 { x: f32, y: f32, z: f32 }          // frozen, inline, never evolves

table Entry {
  account @0: u64;
  amount  @1: i64;
  memo    @2: string?;
}

union Payload { Transfer @1: Transfer, Refund @2: Refund }

table Transaction {
  id       @0: u128;
  currency @1: Currency = BRL;                   // default
  entries  @2: [Entry];
  note     @3: string?;
  @4: reserved;                                  // removed field
  payload  @5: Payload;
}

6.1 Compatibility (lockfile)

The compiler keeps <schema>.zidl.lock with the computed layout. On recompilation:

Error

Warning (breaking for old readers, but an explicit failure, never wrong data)

Allowed

7. Code structure (Cargo workspace)

8. Benchmarks and the winning criterion

Scenarios

  1. Financial transaction: scalars, monetary u64/i64, strings, a list of entries built bit by bit.
  2. Game frame: hundreds of small entities with Vec3/floats.
  3. IoT telemetry: a tiny message (size dominates).
  4. Streaming: large bytes blobs.

Metrics: write time (warm pool), read time (all fields; one random field), bytes/message, allocations/message (instrumented allocator), cross-version reads (v1 reads v2 and v2 reads v1 — correctness, not just time).

Winning: win on writing and size; tie or win on reading; zero allocations per message on the hot path.

Fair comparison (every variant reported, none hidden): reads with and without a verifier/traversal limit in every format; Cap'n Proto size packed and unpacked; zidl strings with UTF-8 validation (default) and _unchecked.

9. Preparing for JS (future)

A JS reader = a lazy view over an ArrayBuffer via DataView; u64/i64 ⇒ BigInt; strings via TextDecoder. Nothing in the format may require alignment or features unavailable in JS. A WASM build of the Rust runtime is a later alternative.

10. Suggested phases

  1. Runtime + hand-written "generated" code for the benchmark schemas.
  2. Benchmarks vs FlatBuffers/Cap'n Proto — validate the format's hypotheses (no padding, i32 offsets, shared bits) before investing in the compiler.
  3. .zidl compiler + Rust codegen + lockfile.
  4. Evolution test matrix and fuzzing of the reader/verify.

11. Left to the builder

Pool implementation, verify default limits, the concrete .zidl syntax, API names, internal module organization, the fuzzing tool.