λHaskell carts are built with wasm32-wasi-ghc in reactor mode. Everything is in IO, and it genuinely works - probably the most surprising thing running on null0.
- Template: cart_haskell - press "Use this template" and you have a working game.
- Docker image:
ghcr.io/notnullgames/null0-cart-haskell:latest - Your code:
cart/Main.hs, compiled with wasm32-wasi-ghc (reactor mode) - Bindings:
carts/haskell/Null0.hs(baked into the docker image for you) - WASI is available, so ordinary file and stdio calls work.
- See carts made with Haskell
- API docs for Haskell
building
You do not install wasm32-wasi-ghc (reactor mode) - 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) \
-v ./cart:/src -v ./webroot:/out \
ghcr.io/notnullgames/null0-cart-haskell:latest mygameThe template wraps that in npm start, which also serves the result and rebuilds when you save.
Haskell notes
- Everything is in
IO. Each null0 call is imported through a generated C trampoline. - Carts get no environment variables, so anything reading
$PWDsees nothing.
a cart, in Haskell
This is carts/haskell/simple/Main.hs in the engine repo - the same file CI builds into the cart below, so it always compiles.
{-# LANGUAGE ForeignFunctionInterface #-}
module Main where
import Null0
-- wasm32-wasi-ghc builds a reactor module, so main is never the entry point
main :: IO ()
main = return ()
foreign export ccall "load" load :: IO ()
load :: IO ()
load = do
clear blue
drawCircle 100 100 50 red
drawText fontDefault "hello from Haskell" 170 92 white
-- callbacks (optional - add a `foreign export ccall` for each one you want)
--
-- foreign export ccall "update" update :: IO ()
-- update :: IO ()
-- update = return ()
carts/haskell/simple/Main.hscallbacks
A cart implements the callbacks it cares about. In Haskell they look like this:
foreign export ccall update :: IO ()See anatomy of a cart for the full list and what each one is passed.