Schema. A new open-source library for serialized data in C++/C#/Golang and Rust

gafferongames1 pts0 comments

schema/README.md at main · mas-bandwidth/schema · GitHub

//blob/show" data-turbo-transient="true" />

Skip to content

Type / to search

Sign in<br>Sign upAppearance settings

You signed in with another tab or window. Reload to refresh your session.<br>You signed out in another tab or window. Reload to refresh your session.<br>You switched accounts on another tab or window. Reload to refresh your session.

Dismiss alert

{{ message }}

Uh oh!

There was an error while loading. Please reload this page.

mas-bandwidth

schema

Public

Notifications<br>You must be signed in to change notification settings

Fork

Star

FilesExpand file tree

main

/README.md

Copy path

Blame<br>More file actions

Blame<br>More file actions

Latest commit

History<br>History<br>History

146 lines (120 loc) · 6.75 KB

main

/README.md

Copy path

Top

File metadata and controls<br>Preview

Code

Blame

146 lines (120 loc) · 6.75 KB

Raw<br>Copy raw file<br>Download raw file

OutlineEdit and raw actions

schema

Write down your data types once and generate code to read and write them in four languages automatically.

const MaxHealth = 1000

enum ShipType { Fighter, Corvette, Bomber }

message ShipState {<br>ship_type ShipType<br>position Vec3<br>orientation Quat<br>health int32 [min = 0, max = MaxHealth]<br>at_rest bool<br>if !at_rest {<br>linear_velocity Vec3<br>angular_velocity Vec3

This declaration compiles to C++, C#, Go and Rust code that<br>reads and writes your data types and agree on every bit. Now your native plugin, your Unity client<br>and your Go backend all speak the same language.

Why it exists

Multiplayer games serialize the same data in several languages at once — an<br>engine client here, a dedicated server there, tools and services around them.<br>Every way of solving that costs something:

Hand-written serializers drift. Someone widens a field on one side; the<br>other side keeps reading the old width, and an afternoon goes to a bug that<br>is one bit wide. Separate read and write paths drift too — even within a<br>single language, the reader and the writer are two expressions of one format<br>that nothing forces to agree.

A unified read/write function fixes the drift and costs you elsewhere.<br>Templating one function over a read stream and a write stream is a good C++<br>answer, and it is still slower than the hand-written pair — and it is not<br>available at all in Go, or in most of the languages a game actually has to<br>ship in.

Solving it with heavier template machinery costs compile time , in headers<br>every translation unit includes.

General-purpose formats fix the drift by paying for it on the wire, in<br>bytes and allocations you cannot afford at 60 Hz.

Generating the code takes the fourth path. One declaration produces the reader<br>and the writer, in every language, so they cannot disagree — and because the<br>format is decided at compile time, what comes out is the straight-line code you<br>would have hand-written, not an interpreter walking a schema at runtime.

The agreement is proven rather than promised: every build compiles the corpus<br>in all four languages and byte-compares the results against pinned goldens. If<br>two languages ever differ by one bit, CI says so before you do.

Features

One declaration, four languages — C++, C#, Go and Rust, bit-identical on<br>the wire, reader and writer generated together so they cannot drift.

Bit-packed, not byte-packed — [min = 0, max = 1000] costs 10 bits, not<br>4 bytes. Bounds are part of the type, and the wire cost follows from them.

Branches that cost nothing — if !at_rest { … } omits whole field groups<br>from the wire, back-referencing a bool already sent.

Compressed floats — [min, max, resolution] sends a step index, not a<br>float. A 0–1 throttle at 0.01 costs 7 bits.

Fixed point is a type in the language — fixed(48, 16) is declared like<br>any other field, and the compiler owns both the storage and the wire for it.<br>This is the one you cannot get by being careful with a format that lacks it:<br>floating point is not bit-identical across compilers and architectures, so<br>a simulation that must agree everywhere — lockstep, rollback, deterministic<br>replay, server/client prediction — cannot be built on floats. A fixed value<br>is an integer, and integers add the same on every machine. It is also its own<br>quantization: storage and wire share one integer domain, so there is no<br>separate quantize step to keep in sync.

128-bit integers , ranged like any other, in every target language.

Zero allocation, no runtime reflection — straight-line code reading and<br>writing your own buffers.

Reads validate, always — out-of-range values are refused, not clamped or<br>trusted. Generated readers are built to face hostile packets.

A second, evolution-tolerant wire for config, assets and settings, with<br>reflection and relocatable storage — see WIRES.md.

schema pack compiles directories of JSON into one binary container,<br>validating every value against the schema as it goes.

A canonical source format — every command formats in place, so the<br>protocol id hashes one true form.

Generated...

schema wire file code languages data

Related Articles