🌾Grain is a functional language that targets wasm directly. It is the most unusual cart to write - numbers cross the boundary as raw wasm values - but a good one if you like ML-family languages.
- Template: cart_grain - press "Use this template" and you have a working game.
- Docker image:
ghcr.io/notnullgames/null0-cart-grain:latest(linux/amd64 only - runs emulated on Apple Silicon) - Your code:
cart/main.gr, compiled with grain - Bindings:
carts/grain/null0.gr(baked into the docker image for you) - WASI is available, so ordinary file and stdio calls work.
- See carts made with Grain
- API docs for Grain
building
You do not install grain - the docker image has it, and the current null0 bindings, baked in. There is nothing in your project to keep in sync with the engine.
docker run --rm --user $(id -u):$(id -g) --platform linux/amd64 \
-v ./cart:/src -v ./webroot:/out \
ghcr.io/notnullgames/null0-cart-grain:latest mygameThe template wraps that in npm start, which also serves the result and rebuilds when you save.
Grain notes
from "./null0.gr" include Null0.- Numbers cross the boundary as raw wasm values, so they are written
100n, not100. - Call
Null0.initColors()first thing inload(), thenNull0.blueand friends work.
a cart, in Grain
This is carts/grain/simple/main.gr in the engine repo - the same file CI builds into the cart below, so it always compiles.
module Main
from "runtime/unsafe/wasmi32" include WasmI32
from "./null0.gr" include Null0
// grain numbers cross the boundary as raw wasm values, so they're written
// `100n`. Colors and strings are pointers into memory - initColors() fills in
// the color constants, and the text bytes go in by hand just below.
@unsafe
provide let load = () => {
Null0.initColors()
// past the color constants; null-terminated, like any C string
let textPtr = 65700n
WasmI32.store8(textPtr, 104n, 0n)
WasmI32.store8(textPtr, 101n, 1n)
WasmI32.store8(textPtr, 108n, 2n)
WasmI32.store8(textPtr, 108n, 3n)
WasmI32.store8(textPtr, 111n, 4n)
WasmI32.store8(textPtr, 32n, 5n)
WasmI32.store8(textPtr, 102n, 6n)
WasmI32.store8(textPtr, 114n, 7n)
WasmI32.store8(textPtr, 111n, 8n)
WasmI32.store8(textPtr, 109n, 9n)
WasmI32.store8(textPtr, 32n, 10n)
WasmI32.store8(textPtr, 71n, 11n)
WasmI32.store8(textPtr, 114n, 12n)
WasmI32.store8(textPtr, 97n, 13n)
WasmI32.store8(textPtr, 105n, 14n)
WasmI32.store8(textPtr, 110n, 15n)
WasmI32.store8(textPtr, 0n, 16n)
Null0.clear(Null0.blue)
Null0.draw_circle(100n, 100n, 50n, Null0.red)
Null0.draw_text(0n, textPtr, 170n, 92n, Null0.white)
}
provide let update = () => void
carts/grain/simple/main.grcallbacks
A cart implements the callbacks it cares about. In Grain they look like this:
provide let load = () => { void }See anatomy of a cart for the full list and what each one is passed.