12 KiB
04 — Odin / GUI Integration
This is the highest-risk area of the whole research effort. FlatBuffers has no first-party Odin binding. You must choose one of several integration paths. This doc lays out the realistic options with tradeoffs, plus the networking story for the GUI.
The core problem
Odin can talk to C via foreign blocks (the C ABI is first-class). FlatBuffers
has two C-related access points:
- Google's official
flatcgenerates C++ and C (C#/Java/etc.) — but its C support is a separate project, FlatCC. - FlatCC (
dvidelabs/flatcc) is an independent FlatBuffers compiler + runtime for pure C. It generates_reader.h/_builder.hheaders per schema plus a smalllibflatccrt.aruntime. Works via the C ABI, so Odin'sforeign importcan consume it.
Choosing a path:
flowchart TD
NAT{"primary target is native desktop?"}
NAT -- "yes" --> CGO{"want to avoid C in the build?"}
CGO -- "yes" --> PATHA["Path B · pure-Odin reader<br/>hand-rolled, no C dependency"]
CGO -- "no" --> PATHA
CGO -- "prefer proven lib / less maintenance" --> PATHC["Path A · FFI to FlatCC<br/>bind generated C headers"]
PATHC --> REUSE["Path C · OdinArrow reuse<br/>or borrow its decode patterns"]
NAT -- "no · browser/WASM" --> PATHD["Path D · TS/JS interop<br/>official JS lib → typed arrays into WASM"]
Index of paths:
| Path | Effort | Zero-copy on reads | Notes |
|---|---|---|---|
| A: FFI to FlatCC (C runtime) | Medium | ✅ | Bind generated C headers to Odin foreign |
| B: Pure-Odin reader (hand-rolled) | High | ✅ | Port a minimal reader; no C dependency |
| C: OdinArrow reuse | Medium-High | ✅ | OdinArrow already has hand-rolled FlatBuffers encoder/decoder |
| D: TS/JS interop (WASM-only) | Low-N/A | Partial | Browser builds can read FlatBuffers via JS lib instead |
Path A — FFI to FlatCC (recommended starting point)
Mechanics. FlatCC generates per-schema C headers. You:
- Install/build flatcc (it's a small C project,
flatbuffer-compatible). - Generate C reader+builder headers from your
.fbs:yieldsflatcc --common -a schema/catalog.fbs -o gui/src/generatedcatalog_reader.h,catalog_builder.h,catalog_verifier.h, plus aflatccrt.h/libflatccrt.aruntime. - Write (or generate with
odin-c-bindgen/Breush/odin-binding-generator) Odin foreign bindings for the handful of functions your GUI actually uses.
Illustrative Odin binding shape (rough, not final API):
package catalog_fb
foreign import flatcc "libflatccrt.a"
@(default_calling_convention = "c")
foreign flatcc {
// table accessors generated by flatcc look like:
flatbuffers_verify_buffer :: proc(buf: rawptr, size: uint, id: ^byte) -> c.int ---
catalog_Catalog_object_count :: proc(t: ^catalog_Catalog_table) -> u64 ---
catalog_CatalogObject_ra :: proc(t: ^catalog_CatalogObject_table) -> f64 ---
// etc.
}
Zero-copy: flatcc's generated reader macros operate directly on the buffer —
ra() is a macro expanding to a bounds-checked buffer read. That maps naturally
to Odin's #foreign + cstring/^f64 access.
Downsides:
- Odin bindings must track schema regeneration. Every time you add a field
to the
.fbs, the C headers change and the Odin foreign decls (or the generated bindings) must be refreshed. - flatcc's API is oriented to C macros; binding it faithfully through Odin
foreignis doable but fiddly (macros don't transfer — you translate each macro into the equivalent C function or hand-roll the offset arithmetic). - You now have a C runtime (static lib) in the GUI build. For the WASM target this must compile under Emscripten — flatcc is plain C and does build for WASM, but adds to WASM binary size and toolchain coupling.
When to choose: when you want a proven library, don't mind C in your Odin build, and want to avoid writing and maintaining a reader yourself.
Path B — Pure-Odin reader (hand-rolled, most effort, most control)
FlatBuffers read access is genuinely simple — as the ODINARROW project proves.
OdinArrow ships a "hand-rolled FlatBuffers encoder/decoder" for the Arrow IPC
header format. A minimal FlatBuffers table reader in Odin is ~100-300 lines:
follow u32 offsets, deref vtables, read little-endian scalars.
What you'd implement:
ReadRoot(root: ^u8, size: uint) -> root_offset(uoffset at byte 0; skip 4-byte length prefix if size-prefixed).- Vtable lookup: given a table addr, read
uoffsetback to vtable, scan slots for a field id, read field offset (or treat as absent → default). - Vectors: u32 length + element stride; for
f64/f32/structs this is a direct^f64slice after bounds check. - Verification: walk offsets checking bounds/alignment (or skip for trusted data — but you're on a network stream; verify, at least bounds, before use).
Downsides:
- You own correctness, update discipline, and testing. Every FlatBuffers format nuance (file identifiers, size prefixes, unions) must be re-implemented.
- Risk of subtle divergence from the C++/Rust/Java implementations (endianness, alignment, default-value semantics).
- Cross-language conformance tests (see
08-testing-strategies.md) absolutely required — you're re-implementing a spec.
When to choose: when you want zero C in the Odin build, plan long-term maintenance, and value full control (and you can lean on OdinArrow's already proven patterns).
Path C — OdinArrow reuse (hybrid)
- https://github.com/TimeLord/OdinArrow — a mature-ish Odin implementation of Apache Arrow's IPC format, including a hand-rolled FlatBuffers encoder/decoder (Arrow IPC metadata is FlatBuffers).
- You could extract/adapt OdinArrow's FlatBuffers decode machinery for your own schema, or (bolder) adopt Arrow IPC entirely for the data path (Arrow IPC is FlatBuffers-framed + columnar buffers — arguably an excellent fit for streaming galaxy positions/redshifts).
- OdinArrow is a small, MIT-style community project (TimeLord). Verify license and maintenance before depending on it.
- If you go pure Arrow IPC, you get batch semantics for free (schema message → record batch messages) — same framing pattern as FlatBuffers with the columnar layout built in.
- Arrow IPC stream = length-prefixed (u32 LE) messages, with a
continuation marker 0xFFFFFFFFfor 4-byte alignment. This is a well-specified framing you can reuse without adopting Arrow's data model.
When to choose: when Arrow-style columnar data is actually what you want (millions of numeric rows — it is a great fit), and you're okay depending on / contributing to OdinArrow.
Path D — WASM/JS interop (browser build only)
- The Web GUI (
gui/www/) builds Odin to WASM and runs alongside JS. - FlatBuffers has official JS/TS support (
flatbuffersnpm package). For the web build you could do the parsing/decoding in JS (or TypeScript) and hand plain arrays (Float64Array) to the Odin WASM side — losing zero-copy at the WASM boundary but gaining ecosystem-tested parsing. - Practical hybrid: WASM build path: keep the WebSocket in JS, decode
FlatBuffers in JS (official lib), then transfer typed arrays into WASM memory
(single
Emscripten.HEAPF64.set(...)copy). Zero-copy is not preserved across the WASM boundary, but the raw-bytes → arrays decode in JS is still far cheaper than JSON and uses a battle-tested library. - Native desktop build (
odin build+ raylib, the primary target): need one of A/B/C. The WEB GUI is secondary.
Recommendation for this repo's roadmap: Start with Path B or A for the
native Odin build (the primary make run target), and use Path D for the
WASM build if/when it becomes a shipping concern. The conformance-test suite
(shared static .mon/.bin fixture files read by both Rust and Odin) is the
safety net that makes the hand-rolled Path B safe.
Networking from Odin
There is no networking code in the GUI today. Options for receiving FlatBuffers from the Rust API:
| Option | Fit | Notes |
|---|---|---|
core:net (Odin stdlib) |
Native desktop | Built-in core:net module has socket APIs; HTTP is manual or minimal — fine for GET of a binary body; WebSocket requires hand-rolling the upgrade + frame handling (doable, ~200 lines) |
Curl FFI (libcurl) |
Native desktop | Battle-tested HTTP, easy buffer callback for application/octet-stream; Odin #foreign to curl is well-trodden (e.g., furbs). Adds libcurl dep to native build |
Emscripten fetch bridge |
WASM | In the browser build, JS owns the network; call fetch from JS or via Odin's Emscripten bindings, then HEAP-copy |
| WebSocket via JS | WASM | Same as above for the browser |
| Community libs | Both | e.g. various core:net-based or thirdparty HTTP clients; vet for maturity |
gui/src/data.odin already has the intended procedure signatures:
get_catalogs :: proc(url: string) -> ([dynamic]Catalog, ^APIError)
get_catalog_objects :: proc(url: string, catalog_name: string) -> ([dynamic]CatalogObject, ^APIError)
When the transport is in place, these become the seam between the network layer and the FlatBuffers decode layer: fetch bytes → verify → read fields → return typed Odin slices.
ZSS / zero-copy in the render loop
The rendering win only materializes if data stays zero-copy into the frame loop:
flowchart LR
A["fetch frame bytes → [dynamic]u8<br/>or a slice pinned for the frame"]
B["verify the buffer once"]
C["ObjectBatch.ra(&buf) → []f64 view"]
D["per object in update()/draw()<br/>ra[i] · dec[i] · z[i] → rl.Vector3 → DrawPoint3D"]
A --> B --> C --> D
No per-object allocation. The current Galaxy { position, color } dynamic array
in main.odin is the data structure you'd replace with slices into the
FlatBuffer.
WASM memory-model caveats
- If JS decodes FlatBuffers and hands typed arrays to Odin/WASM, the copy into
WASM linear memory is one
HEAPF64.set()— a single memcpy, not per-field. - If Odin/WASM itself decodes FlatBuffers over its own
core:netbinding, it reads directly from WASM heap — same as native, but you're now maintaining the hand-rolled decoder in WASM too (subject to WASM's 32-bit indexing, still fine for sub-2GiB buffers). - Emscripten
-sALLOW_MEMORY_GROWTHand 4GB heap settings matter if you plan to hold multi-GB catalogs; keep to batched frames (e.g. ≤ 64 MB) and free per chunk.
Build integration in this repo
The gui/Makefile owns the Odin targets. Adding FlatBuffers means:
- A
-collection:lib=lib/local(or a vendored submodule undergui/lib/) for either theflatccruntime lib or the hand-rolled Odin package. - A
schema-gensource of truth: run flatc for Rust + C (Path A) or otherwise regenerate, as a make target (see README root /03-rust-integration.md). - CI (
odin testin the Gitea workflow) must pick up the new collection and any generated files. Commit generated files to avoid CI toolchain surprises.
References
- Odin FFI docs: https://odin-lang.org/docs/ffi/ (foreign blocks, calling conventions)
- Odin binding to C (official news post): https://odin-lang.org/news/binding-to-c/
- FlatCC: https://github.com/dvidelabs/flatcc (and
flatbuffers.dev/languages/c/) - OdinArrow: https://github.com/TimeLord/OdinArrow (owned FlatBuffers + Arrow IPC)
- Odin binding generator (community): https://github.com/karl-zylinski/odin-c-bindgen
- msgpack-odin (reference for a hand-rolled binary codec in Odin, small): https://github.com/tgolsson/msgpack-odin