Grain

  • 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 mygame

The 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, not 100.
  • Call Null0.initColors() first thing in load(), then Null0.blue and 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.gr

callbacks

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.