A build tool for compiling Odin programs to WebAssembly. wodin parses your source files, extracts JavaScript bindings declared directly in your Odin code, and generates a ready-to-serve HTML page — no JavaScript knowledge required on the Odin side, and no Odin knowledge required on the HTML side.
wodin walks your Odin source tree and looks for foreign blocks whose declarations are tagged with @(tag = ...) attributes. It uses Odin's own core:odin/ast and core:odin/parser packages to do this statically — no compilation needed for the scan.
From those tags it generates a JavaScript import object (_wodin) that the Odin WASM runtime uses to resolve your foreign function calls at load time. It then compiles your package to WASM, copies odin.js from your Odin installation, and injects everything into an HTML template at a marker you control.
The result is a self-contained out/ directory you can serve anywhere.
wodin is a single Odin source file. You can use it however is most convenient for your workflow.
Add to PATH — build once and use it anywhere:
odin build . -out:wodin
# move the binary somewhere on your PATHVendor into your project — copy wodin.odin into your repo and build it where you need it, no PATH changes required:
odin build wodin.odin -file -out:wodin
./wodin build myappThis is handy for reproducible project setups or CI, since the tool lives alongside the code it builds.
wodin <command> [directory] [flags] [-- <odin flags>]
| Command | Description |
|---|---|
build |
Compile and generate output files |
run |
Build then serve locally over HTTP |
help |
Print usage information |
| Flag | Default | Description |
|---|---|---|
-out:<dir> |
out |
Output directory |
-template:<file> |
built-in | Path to a custom HTML template |
-port:<n> |
8080 |
Port for wodin run |
-- |
Everything after this is passed directly to odin build |
wodin recognises two tag prefixes inside foreign blocks.
Declares a JavaScript function that Odin can call. The body of the function is written inline in the tag, in JavaScript:
foreign import canvas "canvas"
foreign canvas {
@(tag = `wodin.function { return a + b }`)
add :: proc "c" (a, b: i32) -> i32 ---
@(tag = `wodin.function { document.getElementById("app").textContent = msg }`)
set_text :: proc "c" (msg: i32) ---
}Parameter names are taken from the Odin proc signature and forwarded to the JS arrow function automatically, so you can reference them by name in the body.
The generated JavaScript for the above looks like:
var _wodin = {
"canvas": {
add: (a, b) => {return a + b},
set_text: (msg) => {document.getElementById("app").textContent = msg},
},
};Declares a JavaScript let variable that your wodin.function bodies can close over. This is the idiomatic way to share mutable state between Odin and JS — the variable itself is not a WASM import; instead you write getter and setter functions that read and write it.
Note that Odin requires variable declarations in an anonymous foreign block (no import name):
foreign import canvas "canvas"
// Variables must be declared in an anonymous foreign block
foreign {
@(tag = `wodin.variable`)
score :: i32 ---
@(tag = `wodin.variable = 0`)
lives :: i32 ---
}
// Functions can then close over those variables
foreign canvas {
@(tag = `wodin.function { return score }`)
get_score :: proc "c" () -> i32 ---
@(tag = `wodin.function { score = v }`)
set_score :: proc "c" (v: i32) ---
@(tag = `wodin.function { return lives }`)
get_lives :: proc "c" () -> i32 ---
}This generates:
let score;
let lives = 0;
var _wodin = {
"canvas": {
get_score: () => {return score},
set_score: (v) => {score = v},
get_lives: () => {return lives},
},
};A foreign block containing only wodin.variable tags (no functions) will have its let declarations emitted but will not produce an entry in _wodin.
By default wodin uses a minimal built-in template:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><!-- WODIN_NAME --></title>
</head>
<body>
<!-- WODIN_BINDINGS -->
</body>
</html>You can supply your own template with -template:<file>. A valid template must contain the <!-- WODIN_BINDINGS --> marker. Everything Odin-related is injected there — the runtime script, the bindings, and the WASM bootstrap — so authors don't need to know anything about the Odin runtime.
| Marker | Description |
|---|---|
<!-- WODIN_BINDINGS --> |
Required. Replaced with the error div, odin.js (unless already present in the template), the let variable declarations, and the _wodin import object. |
<!-- WODIN_LOADER --> |
Optional. When present, the WASM bootstrap script is injected here. When absent, no loader is injected at all — useful if you want to handle loading yourself. |
<!-- WODIN_NAME --> |
Optional. Replaced with the app name (the source directory's basename). Can appear any number of times. |
Each piece of the stack is opt-in. A bindings-only template gives you the _wodin object and nothing else; adding <!-- WODIN_LOADER --> anywhere brings in the bootstrap. If your template already has a <script src="odin.js"> tag, wodin detects it and skips injecting a second one.
A template that interleaves custom setup code between the bindings and the loader:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><!-- WODIN_NAME --> — My Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="canvas"></canvas>
<!-- WODIN_BINDINGS -->
<script>
// runs after _wodin is defined but before the wasm module loads
setupEngine(document.getElementById("canvas"));
</script>
<!-- WODIN_LOADER -->
</body>
</html>A bindings-only template where loading is handled manually:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><!-- WODIN_NAME --> — My Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1><!-- WODIN_NAME --></h1>
<canvas id="canvas"></canvas>
<!-- WODIN_BINDINGS -->
</body>
</html>wodin build myappCompiles myapp/ to out/myapp.wasm, generates out/index.html and copies out/odin.js.
wodin build myapp -out:distwodin run myapp
# wodin: building myapp...
# wodin: serving on http://localhost:8080 (Ctrl-C to stop)wodin run myapp -port:3000wodin build myapp -template:templates/index.htmlAnything after -- is forwarded verbatim to odin build:
wodin build myapp -- -debug -vet
wodin run myapp -- -o:speed -no-bounds-checkThe fixed wodin flags (-target:js_wasm32 and -out:) are always prepended; your extra flags come after.
out/
├── index.html — generated from your template
├── odin.js — Odin WASM runtime (copied from your Odin installation)
└── myapp.wasm — compiled WebAssembly module
The out/ directory (or whatever -out: points to) is deleted and recreated on every build.
- wodin locates
odin.jsby runningodin rootand checkingvendor/wasm/js/odin.jsandcore/sys/wasm/js/odin.js. If neither is found it falls back to theODIN_ROOTenvironment variable. - The built-in HTTP server (
wodin run) is single-threaded and intended for local development only. - WASM modules require certain HTTP headers to use
SharedArrayBuffer. wodin's server setsCross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corpon every response automatically.