Ingredients for cooking Haskell wasm apps
Cheng Shao
June 7, 2025
Lesson 1: your first wasm module
- Let’s build the
pointfree
CLI
- Use
wasm32-wasi-cabal
to build the
executable component
What is a wasm module
- Write once run anywhere, hopefully done right this
time
- A quick look at disassembled module
- Key
concepts
- Linear memory
- Import/export functions
- WASI
- Module/instance
Running a wasm module
- Use
wasmtime
- Only works for self-contained
wasm32-wasi
module
- In a browser
Running pointfree.wasm
in a browser
Optimizing pointfree.wasm
- Use
wasm-opt
to optimize pointfree.wasm
- Use
brotli
to estimate transferred
wasm module size
- Comparing size against native
- Unoptimized
pointfree.wasm
:
8.5M
- Optimized
pointfree.wasm
:
3.6M
- x86_64-linux
pointfree
with split
sections: 14M
Lesson 2: basic JSFFI
- Let’s port
hledger-lib
to the browser
- Need to build the wasm module & a companion JS
module
A closer look at demo code
foreign import javascript unsafe
foreign export javascript
- Special link-time GHC options
Basic foreign types in JSFFI
- Boxed foreign types:
Int
,
Ptr
, etc
JSVal
in GHC.Wasm.Prim
- Can be any JS value
- Representation: unique key in a mapping + weak
pointer
- Garbage collected, can be eagerly freed via
freeJSVal
JSString
and other
newtype
s of JSVal
foreign import javascript unsafe
- Call a synchronous JS function
- Source snippet: JS expression or statements (with
return
to return the result)
- Use
$1
, $2
etc for
Haskell function arguments
- Arguments & result value are fully
evaluated
foreign export javascript
- Default
main
doesn’t work, at least
one export is required as entry point
- Synchronous export
- Async export (default)
- Enable calling async JS function, e.g.
fetch
Using ghci & ghcid
wasm32-wasi-cabal repl
- ghci browser mode
Lesson 3: get your cabal project tested
Lesson 4: advanced JSFFI
- Service
Worker in the demo
- Intercepts
fetch
calls as poor man’s
backend in the frontend
/sha256
: use Web Crypto API to hash
the request payload
- Haskell demo
- Marshaling buffers in JSFFI
- Import async JS function like
fetch
Marshaling buffers
- In JSFFI code,
__exports.memory
is a
Memory
object
- Always use pinned
ByteArray#
- Pay attention to buffer lifetime
- Linear memory may grow during GC, previous buffer
is detached
- Make sure the buffer is fully consumed in JSFFI
code
- Exercise: marshaling
text
,
aeson
foreign import javascript safe
- Call an async JS function that returns a
Promise
await
allowed in source snippet
- Returns a thunk immediately
- When the thunk is forced, blocks current thread
until
Promise
is fulfilled
- Doesn’t block other Haskell threads
- Allows concurrency without threading overhead
- JS exception can be caught as Haskell
exception
Lesson5: a 2D canvas
example
- Draw a simple
canvas
2D animation
- Use
JuicyPixels
to represent each frame
foreign import javascript "wrapper"
- Dynamically export a Haskell function closure as a
JS callback
- Sync/async variants similar to static exports
- See
FunPtr
for the same thing in C FFI
Future work
- In the GHC land
- Threaded RTS & parallelism (#25442)
- Faster ghci (#25407)
- Eventlog & profiling in browser
- In the user land
- Official library for efficiently marshaling
text
, aeson
, etc
- More user-friendly post-linker
- TypeScript bindgen
- Tell me your pain spots & wishlist!