The Wayback Machine - https://web.archive.org/web/20241122110831/https://www.tushar-adhatrao.in/blogs/haskell_to_wasm.html

Compiling Haskell to WebAssembly: A Step-by-Step Guide

An illustration of Haskell and WebAssembly

Introduction

This guide demonstrates how to compile Haskell code into WebAssembly (Wasm). You'll learn how to convert a single Haskell source file to WebAssembly and integrate it into a web application. Additionally, we'll cover compiling a Haskell Cabal project for WebAssembly, including handling data types like strings.

For detailed technical documentation, refer to the GHC WebAssembly user guide.

Environment Setup

Installing the Custom GHC for WebAssembly

To compile Haskell to WebAssembly, you need a custom GHC version that supports the wasm32-wasi target. Detailed installation instructions can be found in the GHC WebAssembly meta repository. Here’s a quick setup guide:

  1. (Optional): Choose a GHC flavor.
    This determines the specific GHC version. For stability, we'll use GHC 9.8.3:

    export FLAVOUR=9.8
    
  2. Run the installation script:

    curl https://gitlab.haskell.org/ghc/ghc-wasm-meta/-/raw/master/bootstrap.sh | sh
    
  3. Update your environment:

    source /home/username/.ghc-wasm/env
    
  4. Verify the installation:

    wasm32-wasi-ghc --version
    wasm32-wasi-cabal --version
    

Compiling a Simple Haskell File to WebAssembly

Writing the Haskell Code

Let’s start with a simple Haskell function that takes an integer and adds 10:

-- Test.hs
addTen :: Int -> Int
addTen n = n + 10

main :: IO ()
main = mempty

However, JavaScript and Haskell use different number types. To bridge this gap, we’ll use C FFI. Here’s the updated code:

-- Test.hs
foreign export ccall addTen :: Int -> IO Int

addTen :: Int -> IO Int
addTen n = return $ n + 10

main :: IO ()
main = mempty

Compiling the Code

Compile your Haskell file to WebAssembly:

wasm32-wasi-ghc Test.hs -optl-Wl,--export=hs_init,--export=addTen

If successful, this will generate a Test.wasm file.


Running WebAssembly in a Browser

Creating an HTML File

Now that you have a WebAssembly module, let’s run it in the browser. Create an index.html file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Haskell + WASM</title>
</head>
<body>
    <h1>Haskell + WebAssembly</h1>
</body>
<script type="module">
    import { WASI } from 'https://cdn.jsdelivr.net/npm/@bjorn3/[email protected]/+esm';

    const wasi = new WASI([], [], []);
    const wasm = await WebAssembly.compileStreaming(fetch("Test.wasm"));

    const inst = await WebAssembly.instantiate(wasm, {
        "wasi_snapshot_preview1": wasi.wasiImport,
    });

    wasi.initialize(inst);
    inst.exports.hs_init(0, 0);
    console.log(inst.exports.addTen(23)); // Outputs: 33
</script>
</html>

This script initializes the Haskell runtime and calls the addTen function.


Compiling a Cabal Project to WebAssembly

Next up, we will build a Haskell cabal project and will use strings instead of integer. as it is slightly more complicated to marshall Javascript strings.

Creating a New Cabal Project

Let’s extend this by creating a Cabal project that handles strings:

mkdir haskell_wasm
cd haskell_wasm
cabal init

Note: You need to initialize project with normal cabal tool that you can install from GHCup.

Writing the Haskell Code

Here’s a simple function to greet a user:

-- app/Main.hs
module Main where

import Foreign.C.String (CString, peekCString, newCString)
import Foreign.Marshal.Alloc (callocBytes, free)
import Foreign.Ptr (Ptr)

foreign export ccall "hs_sayHello" sayHello :: CString -> IO CString
foreign export ccall "callocBuffer" callocBuffer :: Int -> IO (Ptr a)
foreign export ccall "freeBuffer" freeBuffer :: Ptr a -> IO ()

sayHello :: CString -> IO CString
sayHello cstr = do
    inputStr <- peekCString cstr
    newCString $ "Hello, " <> inputStr

callocBuffer :: Int -> IO (Ptr a)
callocBuffer = callocBytes

freeBuffer :: Ptr a -> IO ()
freeBuffer = free

main :: IO ()
main = putStrLn "Hello, Haskell!"

Updating the Cabal File

Add the following ghc-options in your Cabal configuration to export the necessary functions:

executable haskell-wasm
    main-is:          Main.hs
    build-depends:    base ^>=4.19.1.0
    hs-source-dirs:   app
    default-language: Haskell2010
    ghc-options: -optl-mexec-model=reactor -optl-Wl,--export=hs_init,--export=hs_sayHello,--export=freeBuffer,--export=callocBuffer

Building and Running

Build the project:

wasm32-wasi-cabal build

Copy the generated .wasm file into your project directory:

$ cp dist-newstyle/build/wasm32-wasi/ghc-9.8.3.20241108/haskell-wasm-0.1.0.0/x/haskell-wasm/opt/build/haskell-wasm/haskell-wasm.wasm .

Update your index.html to use the hs_sayHello function:

<script type="module">
    import { WASI } from 'https://cdn.jsdelivr.net/npm/@bjorn3/[email protected]/+esm';

    const wasi = new WASI([], [], []);
    const wasm = await WebAssembly.compileStreaming(fetch("haskell-wasm.wasm"));

    const inst = await WebAssembly.instantiate(wasm, {
        "wasi_snapshot_preview1": wasi.wasiImport,
    });

    function callWithString(func, str) {
        const encoder = new TextEncoder();
        const decoder = new TextDecoder("utf8");

        const encoded = encoder.encode(str + '\0'); // Null-terminated string
        const ptr = inst.exports.callocBuffer(encoded.length);
        new Uint8Array(inst.exports.memory.buffer, ptr, encoded.length).set(encoded);

        const resultPtr = func(ptr);
        const resultBytes = new Uint8Array(inst.exports.memory.buffer, resultPtr);
        const length = resultBytes.findIndex(b => b === 0);
        const result = decoder.decode(new Uint8Array(inst.exports.memory.buffer, resultPtr, l));

        inst.exports.freeBuffer(ptr);
        inst.exports.freeBuffer(resultPtr);

        return result;
    }

    wasi.initialize(inst);
    inst.exports.hs_init(0, 0);
    console.log(callWithString(inst.exports.hs_sayHello, "John")); // Outputs: Hello, John
</script>

Conclusion

Congratulations! You’ve successfully compiled Haskell to WebAssembly and integrated it into a web app. This guide covered the basics, but there’s much more to explore, including advanced data types and optimizations.