The cross-language binary
A one-byte header, a type ID, then the body. Varints where the width varies, little-endian where it does not, and a reference flag in front of every value that may be shared.
Cross-language serialization
Apache Fory fixes its wire format apart from any one implementation.
This is that format for MoonBit: the xlang binary, the row layout, and
the type table between them — written from the specification, proved
against pyfory.
What is here
A serialization format is a promise about bytes. Fory keeps two: the cross-language binary that moves a value between runtimes, and the row layout that lets a reader take one field without rebuilding the object. Both sit on the same mapping from MoonBit types to Fory's type IDs.
A one-byte header, a type ID, then the body. Varints where the width varies, little-endian where it does not, and a reference flag in front of every value that may be shared.
A null bitmap, eight bytes per field, then the variable data. Field
i is always at bitmap + i × 8, so a reader
takes one field and leaves the rest unread.
A value written twice is written once and pointed at afterwards. MoonBit has no object identity to track, so sharing is marked with a key you choose; a cycle that cannot be resolved is refused, not hung on.
Date, Moment and Span travel
as DATE, TIMESTAMP and DURATION. Nanoseconds stay the sub-second
part, in [0, 1e9), never a negative remainder.
The shape of the API
dumps and loads move a whole message.
dump and load do the same over a buffer or
a reader you already hold. A type joins in by implementing
Codec — one method to write the body, one to read it.
| Call | Result |
|---|---|
dumps(v) |
one message as Bytes |
loads(b) |
the value, or a Refused |
dump(v, buf) |
writes into your buffer |
load(reader) |
the next value |
let bytes : Bytes = @moonfory.dumps(42) // 01 ff 04 2a 00 00 00 let back : Int = @moonfory.loads(bytes) // 42 pub impl Codec for UserId with fn ty(_) { TypeId::Int64 } pub impl Codec for UserId with fn write_data(self, w) { w.write_i64(self.0) }
Row format
Give the schema once. Set fields by index, finish, and open a cursor over the bytes. Reading field 1 never touches field 0.
let schema = @moonfory.RowSchema::new([ { name: "id", ty: @moonfory.RowType::Int64, nullable: false }, { name: "name", ty: @moonfory.RowType::String, nullable: false }, { name: "age", ty: @moonfory.RowType::Int32, nullable: true }, ]) let row = @moonfory.Row::new(schema) row.set(0, @moonfory.RowValue::Int64(7L)) // id row.set(1, @moonfory.RowValue::String("ada")) // name row.set(2, @moonfory.RowValue::Null) // age let cur = @moonfory.RowCursor::new(row.finish()[:], schema) cur.get_field(1) // "ada" — field 0 never read cur.is_null(2) // true
89 tests, every one a byte fixture: our own, and the ones
pyfory writes. Every refusal has a test that provokes
it.
wasm, wasm-gc, js and
native, checked with --deny-warn in CI on
Linux, macOS and Windows.