A C-header-to-Odin bindings generator, written in Odin.
H2Odin reads C headers with libclang and produces clean, idiomatic Odin bindings — configured through a small but powerful Lua policy layer.
Status: usable pipeline (Milestones 0–5 + 7–12). Idiomatic wrappers (Milestone 6) are deferred.
- Two type modes. ABI mode preserves the C API faithfully using Odin's C-compatible types (
c.int,c.size_t, …). Idiomatic mode generates native Odin types (i32,f64, …) where the substitution is proven ABI-safe on the target. Generated wrapper procedures (e.g.cstring → stringparams,pointer+length → []Tslices) are planned, not yet implemented. - Measured C bit-fields. Representable bit-field runs become Odin
bit_fieldregions only when their size, alignment, and offsets can be proven from libclang's target layout; everything else fails closed to an opaque record with a diagnostic. - Correctness first. A type is never swapped for a nicer-looking one if it would break behavior or the ABI. When the header is ambiguous, H2Odin picks a safe default, flags it, and lets you override it — it never silently guesses wrong.
- Deterministic. Same headers plus the same config tree always produce identical output. The Lua config is sandboxed (no
io/os/debug, no raw loaders);requireonly resolves theh2odinprelude and sibling.luaunder the config directory. - Configurable in Lua. Simple libraries need a few lines of data; tricky ones drop into Lua functions for the hard cases — same small API either way.
- Diagnostics report. Every non-certain decision in a run (guessed pointer lowerings, unresolved idiomatic leaves, opaque layout fallbacks, …) is listed on stderr after generation.
- Odin compiler
- A libclang shared library (the Clang C API), findable by the linker
odinfmtonly if you usemake format
make check # type-check the package
make build # produce build/h2odin
make test # unit + e2e tests (builds the binary first)
make format # odinfmt via odinfmt.jsonH2Odin is self-hosted: the libclang bindings its Extraction stage imports
are generated by H2Odin itself from the pinned headers in
vendored/libclang/headers/. make regen-libclang rebuilds the binary
against the checked-in package and regenerates it in place (see
docs/specs/0002-self-hosted-libclang.md).
./build/h2odin -config:file.lua [-quiet]- A Lua config is required. Headers, type mode, naming, output path, and other policy live there (
config.inputs,config.type_mode,config.output_folder, …). - Generated Odin goes to stdout, or to files under
config.output_folderwhen set (relative paths resolve against the config directory). - Non-certain decisions go to stderr as a single diagnostics report;
-quiet/-qsuppresses that report (error severities still fail the run). -help/-hprints usage.
Example (writes examples/sqlite3/sqlite3.odin via config.output_folder = "."):
./build/h2odin -config:examples/sqlite3/config.luaCheck generated examples:
odin check examples/sqlite3 -no-entry-point -collection:vendored=$(pwd)/vendored
odin check examples/fff -no-entry-point -collection:vendored=$(pwd)/vendored
odin check examples/bit_fields -no-entry-point -collection:vendored=$(pwd)/vendoredConfiguration is a Lua program that require "h2odin", builds a sectioned object with h2o.config(), and returns it. Common cases are plain data; hard cases are callbacks that return a decision, or nil to accept the default.
local h2o = require "h2odin"
local config = h2o.config()
config.package = "raylib"
config.foreign.import_lib = "raylib"
config.type_mode = "idiomatic"
config.naming = h2o.naming.odin {
strip_prefixes = { proc = "gl", type = "GL", const = "GL_" },
override = function(sym)
return sym.default
end,
}
config.types.overrides = { Vector2 = "[2]f32", Color = "distinct [4]u8" }
config.symbols.remove.where = function(sym)
return h2o.str.has_prefix(sym.name, "_")
end
return config| Path | Role |
|---|---|
package / type_mode |
package name; ABI vs idiomatic leaves |
inputs / preprocess.* |
multi-header inputs; -I / -D |
foreign.import_lib / link_prefix |
system library; C symbol prefix |
naming.strip_prefixes |
drop a C prefix by kind (proc / type / const) |
naming.override |
rename callback |
types.map / types.overrides |
type spellings (refs only vs also drop the decl) |
structs.* / procs.* |
field tags/align; param/result spellings and defaults |
output.* / output_folder |
layout (merged/per_header), imports file, footers |
symbols.remove.where |
true drops a top-level declaration |
Unknown keys and not-yet-supported sections (diagnostics) fail the run with a clear error. Pre-M8 flat keys (keep, rename, type_map, …) are rejected with migration messages.
More detail: docs/configuration.md. Full north-star: docs/config-spec.md.
Design and architecture notes live in docs/. Start with CONTEXT.md for orientation and ROADMAP.md for status.
To be decided.