Ingredients for cooking Haskell wasm apps

Cheng Shao

June 7, 2025

About the wasm track

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 newtypes 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

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!

More resources

Q/A