Flat  Buffers:   Generic Reader and Builder
1 A tour of the format
2 Quick start
3 Reading buffers
open-flatbuffer
fb-root
fb-bytes
table-field-present?
table-field-bool
table-field-u8
table-field-u16
table-field-u32
table-field-u64
table-field-i8
table-field-i16
table-field-i32
table-field-i64
table-field-f32
table-field-f64
table-field-string
table-field-table
table-field-vector
table-field-struct
fb-vector
fb-vector-length
fb-vector-ref-u8
fb-vector-ref-u16
fb-vector-ref-u32
fb-vector-ref-u64
fb-vector-ref-i8
fb-vector-ref-i16
fb-vector-ref-i32
fb-vector-ref-i64
fb-vector-ref-f32
fb-vector-ref-f64
fb-vector-ref-bool
fb-vector-ref-string
fb-vector-ref-table
4 Building buffers
make-builder
builder-offset
builder-create-string
builder-create-u8-vector
builder-start-vector
builder-end-vector
builder-start-object
builder-add-slot
builder-add-bool-slot
builder-add-u8-slot
builder-add-u16-slot
builder-add-u32-slot
builder-add-u64-slot
builder-add-i8-slot
builder-add-i16-slot
builder-add-i32-slot
builder-add-i64-slot
builder-add-f32-slot
builder-add-f64-slot
builder-end-object
builder-finish
4.1 Alignment primitives
builder-prep
builder-prealign
5 Interoperability with flatc
6 Limitations
9.3

FlatBuffers: Generic Reader and Builder🔗ℹ

turinglambdaai

 (require flatbuffers) package: flatbuffers

This package is a pure-Racket implementation of the FlatBuffers wire format — Google’s zero-copy serialization format. It provides:

  • a generic reader that decodes any FlatBuffers buffer by numeric field id, with no generated bindings and no schema at hand, and

  • a low-level builder that follows the reference builder’s write-back-to-front model, with vtable deduplication and correct per-primitive alignment.

There is no schema compiler and no code generation in this version; schema codegen bindings are future work. There is no FFI. The only dependency is base.

The format implemented here is the core FlatBuffers format as produced and consumed by current flatc releases: scalars are always little-endian, buffers use 32-bit offsets, and buffers are standard (not size-prefixed). The 64-bit-offset variant of the format is not supported. (Little-endian is a property of the format itself, not of this implementation: “Each scalar is also always represented in little-endian format.”)

    1 A tour of the format

    2 Quick start

    3 Reading buffers

    4 Building buffers

      4.1 Alignment primitives

    5 Interoperability with flatc

    6 Limitations

1 A tour of the format🔗ℹ

A FlatBuffers buffer starts with a uoffset (a uint32) pointing forward to the root table. A table starts with an soffset (a signed int32) such that vtable position = table position - soffset; because the value is signed, the vtable may lie before or after the table. A vtable holds a uint16 vtable size, a uint16 table inline size, and one uint16 per field: the field’s offset from the table start, or 0 if the field is absent. Strings are uint32-length-prefixed, null-terminated UTF-8 byte vectors; vectors are uint32-length-prefixed sequences of aligned elements. Strings, vectors, sub-tables and structs are reached through unsigned offsets stored at the field’s position, each relative to its own location.

Two consequences drive this API:

  • Default elision. A field whose value equals its default is simply not stored. A reader must return the default for absent fields — which is why every accessor in this package takes an optional default.

  • Vtable deduplication. Tables with identical layouts share one vtable; the shared vtable may sit after the table (the soffset is then negative). The builder performs this dedup automatically, and the reader handles both directions.

2 Quick start🔗ℹ

Take this schema (the one used by the package’s own tests):

  // test/schema.fbs (excerpt)

  table Inner {

    x:    int32;     // id 0

    y:    int32;     // id 1

    name: string;    // id 2

  }

  

  table Outer {

    label:  string;    // id 0

    inner:  Inner;     // id 1

    inners: [Inner];   // id 2

    tags:   [string];  // id 3

    scalars: Scalars;  // id 4

  }

Building the equivalent buffer with the low-level builder. Strings and vectors must be created before the object that references them; vector elements are prepended in reverse order; field ids are the vtable slot numbers:

> (define b (make-builder))
> (define label-off (builder-create-string b "hello"))
> (define name-off (builder-create-string b "in"))
> (define a-off (builder-create-string b "a"))
> (define b-off (builder-create-string b "b"))
> (define inner-off
    (let ()
      (builder-start-object b 3)
      (builder-add-i32-slot b 0 1)
      (builder-add-i32-slot b 1 -2)
      (builder-add-slot b 2 name-off)
      (builder-end-object b)))
> (define inner-a-off
    (let ()
      (builder-start-object b 3)
      (builder-add-i32-slot b 0 10)
      (builder-add-slot b 2 a-off)
      (builder-end-object b)))
> (builder-start-vector b 2 4)
> (builder-prepend-uoffset b inner-a-off)
> (builder-prepend-uoffset b inner-off)
> (define inners-off (builder-end-vector b 2))
> (define tags-off
    (let ()
      (builder-start-vector b 2 4)
      (builder-prepend-uoffset b b-off)
      (builder-prepend-uoffset b a-off)
      (builder-end-vector b 2)))
> (define root-off
    (let ()
      (builder-start-object b 5)
      (builder-add-slot b 0 label-off)
      (builder-add-slot b 1 inner-off)
      (builder-add-slot b 2 inners-off)
      (builder-add-slot b 3 tags-off)
      (builder-add-slot b 4 0)
      (builder-end-object b)))
> (define buf (builder-finish b root-off))
> (bytes-length buf)

152

Reading it back with the generic reader. Missing fields return the caller’s default (or the type default 0 / 0.0 / #f):

> (define t (fb-root (open-flatbuffer buf)))
> (table-field-string t 0)

"hello"

> (table-field-i32 (table-field-table t 1) 1)

-2

> (define inners (table-field-vector t 2))
> (fb-vector-length inners)

2

> (table-field-i32 (fb-vector-ref-table inners 1) 0)

10

> (table-field-table t 4)

#f

> (table-field-i32 t 4 99)

99

> (table-field-present? t 4)

#f

Buffers built by this builder are ordinary FlatBuffers buffers: they can be verified with flatc -t, read by the official C++/Java/Python runtimes, and so on. The package’s test suite does exactly that (see Interoperability with flatc).

3 Reading buffers🔗ℹ

procedure

(open-flatbuffer bs) → fb?

  bs : bytes?
Validates that bs is large enough for a root offset, that the root offset is in range, and that the root table and its vtable are readable; raises exn:fail on garbage. The byte string is referenced zero-copy — it must not be mutated for as long as any value derived from the buffer is used.

procedure

(fb-root f) → table?

  f : fb?
The root table of the buffer.

procedure

(fb-bytes f) → bytes?

  f : fb?
The underlying byte string (useful for custom zero-copy parsing).

A table pairs a buffer with the table’s absolute byte position; table-pos exposes it. Fields are accessed by numeric field id (the vtable slot, normally the field’s declaration order in the schema):

procedure

(table-field-present? t id) → boolean?

  t : table?
  id : exact-nonnegative-integer?
Is field id present? Per the format, a slot beyond the vtable’s field count counts as absent — this is not an error.

procedure

(table-field-bool t id [default]) → boolean?

  t : table?
  id : exact-nonnegative-integer?
  default : any/c = #f
(table-field-u8 t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = 0
(table-field-u16 t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = 0
(table-field-u32 t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = 0
(table-field-u64 t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = 0
(table-field-i8 t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = 0
(table-field-i16 t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = 0
(table-field-i32 t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = 0
(table-field-i64 t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = 0
(table-field-f32 t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = 0.0
(table-field-f64 t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = 0.0
(table-field-string t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = #f
(table-field-table t id [default]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = #f
Typed field accessors. If the field is absent, default is returned — this is how FlatBuffers default elision reaches the reader. Strings are decoded as UTF-8 (invalid UTF-8 raises exn:fail).

procedure

(table-field-vector t    
  id    
  [default    
  #:elem-size elem-size]) → any/c
  t : table?
  id : exact-nonnegative-integer?
  default : any/c = #f
  elem-size : (or/c #f exact-positive-integer?) = #f
Returns an fb-vector for the field, or default if absent. A generic reader cannot know an element’s type, so the typed element accessors below decide the interpretation — use the accessor that matches the schema. When elem-size is given, the vector’s byte span is validated eagerly.

procedure

(table-field-struct t id size [default]) → any/c

  t : table?
  id : exact-nonnegative-integer?
  size : exact-positive-integer?
  default : any/c = #f
Inline struct fields are returned as a copied byte string of size bytes (little-endian layout); the size must be known from the schema. For zero-copy access, combine table-field-present? with fb-bytes.

struct

(struct fb-vector (fb pos len))

  fb : fb?
  pos : exact-nonnegative-integer?
  len : exact-nonnegative-integer?
A vector in the buffer; pos is the absolute position of the first element.

Element count.

procedure

(fb-vector-ref-u8 v i) → (integer-in 0 255)

  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-u16 v i) → (integer-in 0 65535)
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-u32 v i) → exact-nonnegative-integer?
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-u64 v i) → exact-nonnegative-integer?
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-i8 v i) → (integer-in -128 127)
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-i16 v i) → (integer-in -32768 32767)
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-i32 v i) → exact-integer?
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-i64 v i) → exact-integer?
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-f32 v i) → real?
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-f64 v i) → real?
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-bool v i) → boolean?
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-string v i) → string?
  v : fb-vector?
  i : exact-nonnegative-integer?
(fb-vector-ref-table v i) → table?
  v : fb-vector?
  i : exact-nonnegative-integer?
Typed element accessors, bounds-checked against the vector length and the buffer.

4 Building buffers🔗ℹ

procedure

(make-builder [capacity]) → builder?

  capacity : exact-positive-integer? = 1024
An empty builder. The internal buffer grows as needed.

procedure

(builder-offset b) → exact-nonnegative-integer?

  b : builder?
The number of bytes written so far; this is the builder’s “offset” currency.

procedure

(builder-create-string b str) → exact-nonnegative-integer?

  b : builder?
  str : string?
Encodes str as UTF-8 with the length prefix and NUL terminator ([u32 length][bytes][NUL][padding]), aligned so the length prefix is 4-byte aligned. Must not be called while an object or vector is under construction.

procedure

(builder-create-u8-vector b data) → exact-nonnegative-integer?

  b : builder?
  data : bytes?
Creates a [ubyte] vector.

procedure

(builder-start-vector b num-elems elem-size) → void?

  b : builder?
  num-elems : exact-nonnegative-integer?
  elem-size : exact-positive-integer?
Begins a vector; then prepend the elements in reverse order with the builder-prepend-* primitives, and call builder-end-vector with the same num-elems. elem-size is the per-element byte size (4 for offset vectors, otherwise the scalar’s size).

procedure

(builder-end-vector b num-elems) → exact-nonnegative-integer?

  b : builder?
  num-elems : exact-nonnegative-integer?
Writes the length prefix and returns the vector’s offset.

procedure

(builder-start-object b num-fields) → void?

  b : builder?
  num-fields : exact-nonnegative-integer?
Begins an object with num-fields vtable slots; field ids used by the builder-add-*-slot procedures must be smaller than num-fields.

procedure

(builder-add-slot b field-id offset-value) → void?

  b : builder?
  field-id : exact-nonnegative-integer?
  offset-value : exact-nonnegative-integer?
Adds an offset field (string, vector or table): a uoffset pointing at offset-value. Passing 0 omits the field.

procedure

(builder-add-bool-slot b id v [default]) → void?

  b : builder?
  id : exact-nonnegative-integer?
  v : any/c
  default : any/c = #f
(builder-add-u8-slot b id v [default]) → void?
  b : builder?
  id : exact-nonnegative-integer?
  v : (integer-in 0 255)
  default : (integer-in 0 255) = 0
(builder-add-u16-slot b id v [default]) → void?
  b : builder?
  id : exact-nonnegative-integer?
  v : (integer-in 0 65535)
  default : (integer-in 0 65535) = 0
(builder-add-u32-slot b id v [default]) → void?
  b : builder?
  id : exact-nonnegative-integer?
  v : exact-nonnegative-integer?
  default : exact-nonnegative-integer? = 0
(builder-add-u64-slot b id v [default]) → void?
  b : builder?
  id : exact-nonnegative-integer?
  v : exact-nonnegative-integer?
  default : exact-nonnegative-integer? = 0
(builder-add-i8-slot b id v [default]) → void?
  b : builder?
  id : exact-nonnegative-integer?
  v : (integer-in -128 127)
  default : (integer-in -128 127) = 0
(builder-add-i16-slot b id v [default]) → void?
  b : builder?
  id : exact-nonnegative-integer?
  v : (integer-in -32768 32767)
  default : (integer-in -32768 32767) = 0
(builder-add-i32-slot b id v [default]) → void?
  b : builder?
  id : exact-nonnegative-integer?
  v : exact-integer?
  default : exact-integer? = 0
(builder-add-i64-slot b id v [default]) → void?
  b : builder?
  id : exact-nonnegative-integer?
  v : exact-integer?
  default : exact-integer? = 0
(builder-add-f32-slot b id v [default]) → void?
  b : builder?
  id : exact-nonnegative-integer?
  v : real?
  default : real? = 0.0
(builder-add-f64-slot b id v [default]) → void?
  b : builder?
  id : exact-nonnegative-integer?
  v : real?
  default : real? = 0.0
Typed slot writers with default elision: a value equal? to default is not written at all, exactly matching the reference builders and the reader’s fallback semantics. (NaN never equals its default, so NaN fields are always written.)

procedure

(builder-end-object b) → exact-nonnegative-integer?

  b : builder?
Writes the table: the soffset placeholder, then the vtable (deduplicated against previously written vtables), then patches the soffset. The returned offset is the table’s.

procedure

(builder-finish b root) → bytes?

  b : builder?
  root : exact-nonnegative-integer?
Aligns the total length to the largest alignment used, prepends the root offset, and returns the finished byte string: a standard 32-bit, little-endian, non-size-prefixed FlatBuffers buffer.

4.1 Alignment primitives🔗ℹ

The builder-prepend-* primitives write scalars back-to-front, each aligned to its own size relative to the start of the finished buffer. The two padding primitives are exposed for completeness:

procedure

(builder-prep b alignment) → void?

  b : builder?
  alignment : exact-positive-integer?
Pads so the current offset becomes a multiple of alignment.

procedure

(builder-prealign b future-bytes alignment) → void?

  b : builder?
  future-bytes : exact-nonnegative-integer?
  alignment : exact-positive-integer?
Pads so the offset becomes aligned after future-bytes further bytes are written — the primitive behind string and vector prefix alignment.

5 Interoperability with flatc🔗ℹ

The test suite ("flatbuffers/test/flatbuffers-test.rkt") checks:

  • binaries produced by flatc -b from JSON test data (including a default-elision case and an empty-vector/empty-string edge case) are decoded correctly by the reader;

  • buffers built by the builder are converted back to JSON with flatc -t and compared field-by-field semantically — byte-for-byte equality is not required, because the format deliberately leaves field order, object placement and vtable sharing unspecified;

  • an alignment fuzz loop (strings of length 1..40 interleaved with uint64 fields) round-trips through both our reader and, for one case, flatc;

  • vtable deduplication produces buffers flatc accepts (soffset pointing forward to a shared vtable).

The interop tests are skipped gracefully at runtime when flatc is not on PATH (e.g. on Windows CI), so the pure-Racket round-trip suite always runs.

6 Limitations🔗ℹ

  • Little-endian, 32-bit-offset, non-size-prefixed buffers only.

  • No schema verification: a generic reader trusts the schema knowledge of its caller (a vector’s element type, a struct’s size, ...). Malformed or hostile buffers raise exn:fail on out-of-range access rather than guaranteeing memory safety properties beyond Racket’s own byte-string bounds. A formal verifier is future work.

  • The builder deduplicates vtables byte-exactly (like the reference implementation); tables built with different slot counts produce distinct vtables even if some slots are unused.