Lesson 19 bi format

Generating code

bi gen struct reads a schema and writes the same types in C, C++, Go, Rust, C3 or Lua — with defaults, id constants and the instances themselves, so the schema stays the one source.

The command

A .bischema says what a Weapon is. The game that loads level1.bidata needs the same thing said in its own language, and typing it twice is how the two drift apart.

bi gen struct --lang <lang> -o <dir> -i <file>... [-m <file>...] [--pkg <name>] [--force] [-v]
FlagIs
--lang <lang>c, c++ (or cpp), go, rust, c3, lua — one per run
-o <dir>where the files go; created with its parents
-i <file>a .bischema or .bidata; repeatable — a data file brings its schema along
-m <file>a .bimapping; repeatable, later files winning per key
--pkg <name>the namespace, package or module to declare the types in
--forceoverwrite without asking
-vsay what was decided: the mapping merge, the notes, every file’s fate

-i and -o are required, flags come in any order, and -- ends them.

level1.bidata bi gen struct 1 / 2
bi gen struct: step 0 bi gen struct: step 1

Every -i is read before anything is written, and a file that does not parse stops the run with the same diagnostics the property view shows. Nothing is written when anything is wrong — half-generated code beside code that compiled yesterday is worse than no code at all.

tip
A bare command word lists its subcommands, the way cargo does: bi gen prints the generators, bi help gen the same. See the command line for the rest of them.

What comes out

One file per schema, named after its stem — game.bischema becomes game.rs, game.h, game.go. One file per data file, named the same way, holding the instances. And once per output directory, bi_types.<ext> with the support types the builtins need — colours, curve, gradient, Ref — holding only the ones your schemas actually use.

gen/game.rs The generated code 1 / 3
The generated code: step 0 The generated code: step 1 The generated code: step 2

Each file opens with a line naming where it came from:

// generated by `bi gen struct` from game.bischema — do not edit

Generated text is a pure function of the inputs: types in schema order, imports sorted, no timestamp and no bi version in the header. Regenerating an unchanged schema produces a byte-identical file, which is what lets the output live in git. A file that already holds exactly that text is reported unchanged and left alone; one that differs asks before overwriting, unless --force says not to, and a run with no terminal skips it and exits non-zero.

Types, defaults, ids, instances

Types map to each language’s idiom: an enum becomes a Go string type with constants, a Rust enum with a NAMES table, a Lua table of names — always keeping the wire names, because that is what the data stores. list<T> is a Vec<T>, a []T, a std::vector<T>; optional<T> an Option<T>, a pointer, a std::optional<T>; ref<S> a typed string wrapper, so a Ref<Weapon> cannot be handed to a field expecting a Ref<Enemy>.

Defaults travel with the types, because they are what make sparse data loadable: impl Default in Rust, member initialisers in C++, DefaultWeapon() in Go, weapon_init() in C, weapon_default() in C3, Weapon.new(t) in Lua.

Ids become constants — WEAPON_RUSTY_SWORD, weapon::RUSTY_SWORD, WeaponRustySword — so a ref written in code is checked by the compiler instead of being a string you spelled right. Only the data files on the command line are scanned, never the directory around them, so the same command in a Makefile always produces the same output.

Instances come out too, one file per data file: each instance a constant with its sparse fields filled in from the defaults, plus a table per type and a find(id) that walks it. The loader becomes a lookup, with no I/O and no parsing at runtime. ids = false and instances = false in the mapping turn either off.

LanguageAn instanceIts table
Rustlevel1::weapon::rusty_sword()weapon::all(), weapon::find(id)
Clevel1_weapon_rusty_swordlevel1_weapon_all[], level1_weapon_find
C++level1::weapon::rusty_swordweapon::all, weapon::find(id)
GoLevel1WeaponRustySwordLevel1Weapons, FindLevel1Weapon(id)
C3level1_weapon_rusty_swordlevel1_weapon_all(), level1_weapon_find
LuaM.Weapon.rusty_swordM.Weapon.all, M.Weapon.find(id)

Names are cased per language — Go and Rust get Pascal types, Rust and C3 get snake fields, C gets RARITY_COMMON — and the schema’s own spelling is kept as the wire name wherever the language can hold it: a serde rename, a json: tag, a comment in C. A reserved word, a collision after casing, or a name the language owns is an error naming both sides and the mapping key that fixes it, rather than a silently numbered name.

The mapping file

A .bimapping is TOML, and it answers two questions: what should not be generated because the project already has it, and how the generated code is spelled. Neither changes what the data means.

ids = true                       # id constants from the data files given

[names]                          # what a schema name is called in code
"Weapon.type" = "kind"           # a field
"Rarity.very rare" = "VeryRare"  # an enum value
"Enemy" = "Monster"              # a type; the wire name stays "Enemy"

[rust]
imports = ["use serde::Deserialize;"]
derive  = ["Debug", "Clone", "Deserialize"]

[rust.types]
Vec2 = "glam::Vec2"              # external: not generated, spelled like this
rgb  = { as = "palette::Srgb<u8>", import = "use palette::Srgb;", default = "Srgb::new(0, 0, 0)" }

[rust.generics]
list = "SmallVec<[{T}; 4]>"      # {T} is the element's spelling
ref  = "Handle<{S}>"             # {S} is the target struct

[c]
prefix = "gs_"                   # every type and constant: gs_Weapon, GS_RARITY_COMMON
KeyMeans
importslines placed once at the top of every unit
typesa schema type or builtin → the spelling to use instead of generating it; the long form adds an import and a default
genericslist, optional, ref → a template with {T} or {S}
nameslike [names], for one language; wins over the shared one
headertext after the generated-by line
prefixC only: prepended to every type and constant
deriveRust only: the derive list; Deserialize turns on serde renames

A mapping key naming a type no schema uses is a warning, not an error — one mapping is meant to serve a whole project. An unknown key is an error with its line and file.

bi gen sample mapping writes game.bimapping beside the sample with every key present and commented out, the way bi config init writes the user config, so the file documents itself and changes nothing until you uncomment a line.

tip
An externally mapped type has no default the generator can spell, so that field falls back to the language’s own zero and the run prints a note — or you give the type a default in the mapping.