Skip to main content

Condensed Asterius quickstart

Here's an Asterius quickstart that aims to be a little more make-it-work-oriented than the official docs.

Coding

Asterius is a WebAssembly compiler for Haskell by Tweag. Really, "the only one" — a lot of work has gone into it. It currently allows you to import arbitrary Javascript expressions (pockmarked with $1,$2,... placeholders to substitute Haskell arguments and make an ad hoc function), and export Haskell functions into Javascript. Here are the marshallable types alongside Int and Float. The bulk Asterius as it pertains to Haskell code is in these aspects of the import/export interface:

import Asterius.Types

-- IMPORTS --
-- All results of imports are IO, wrapping either `JSVal` or `()`
-- Argument $1, $2... correspond to the types in the imported signature
-- `unsafe` = synchronous: block until resolved
foreign import javascript unsafe "js_sync_int_fn($1) + $2;" foo :: Int -> Int -> IO Int
-- `safe` = Promise: parallelize where possible
-- also shown: exporting arg $2 to to JS
foreign import javascript safe "(async () => { const a = await js_async_str_fn($1); await $2(a); })()" bar :: JSString -> JSFunction -> IO JSVal

-- EXPORTS --
{- given -} hs_fn :: JSString -> JSArray -> Int
{- export like -} foreign export javascript "js_name_for_hs_fn" hs_fn :: JSString -> JSArray -> Int

-- Note: all Haskell functions exported to JS are async by both the `export` and `import` syntax

As well as creating JSFunction values to export Haskell functions via import as seen above:

-- JSFUNCTION --
-- JSFunction values are created from Haskell functions with the magic polymorphic import `wrapper`:
foreign import javascript "wrapper" fn_wrap_a :: (Int -> JSString) -> JSFunction
foreign import javascript "wrapper" fn_wrap_b :: (JSObject -> Int -> JSArray -> Float) -> JSFunction
-- and so on.
-- a `wrapper oneshot` magic import is available for functions called exactly once then disposed

As an interface this is Asterius's entire job — marshalling data and functions between Javascript and Haskell — and nothing is first-class. Any JS function or value you want, you write an explicit foreign import then shuttle it into a Haskell datatype with only Aeson to help. JS Promises are parallelized as much as they can be before they cross the import boundary and hit the Haskell fabric.

† There is a small limitation on functions that fits in memory under a limit you can set through this compiler flag. The first-class foreign export javascript ... syntax is only supported for non-Cabal projects (for now?). Cabal projects use the flexible import expressions to export, by means of binding into callbacks and ultimately the global scope à la:

foreign import javascript "document.body.addEventListener('click', $1) || undefined" onclick :: JSFunction -> IO ()
foreign import javascript "(window.__hsfn = $1) || undefined" shove :: JSFunction -> IO ()

Compiling

With the code above you can make a Haskell project that's WASM-ready without needing to know anything about the tooling. If you're responsible for compilation, here's what I bet you need to know.

On *nix, their Docker/podman image that contains their custom GHC, Cabal and package store is recommended. Of note are the three functions ahc-cabal as a cabal-install wrapper, ahc-dist to transpile the funky custom pseudo-bytecode from ahc-cabal build to wasm (+ a bunch of boilerplate JS), and finally standalone ahc-link which does as ghc/runhaskell and compiles the Haskell sources from scratch.

After initializing the container in your working directory, you can compile straight away from commandline. Here are a couple of useful workflows for targeting browser. To target Node you remove any mention of "browser" including in the commands.

  1. Compiling a Cabal project: This means you need to use a Cabal package that isn't included in their default package list, or you need some other Cabal "dark arts" as they themselves call it. There aren't too many options to tweak in this path.

    1. Add asterius-prelude to your .cabal build-depends

    2. Run ahc-cabal build, which will compile into dist-newstyle/ as usual.

    3. Locate the "binary" in dist-newstyle/, typically as:

      dist-newstyle/build/<CPU-type-and-OS>/ghc-<version #>/<pkg-name-and-version>/x/<pkg-name>/build/<pkg-name>/<pkg-name>
      

      and symlink the binary for convenience into the project root.

    4. Run ahc-dist on that binary, to spit out the wasm and friends into public/, which will look something like this:

      $ ahc-dist --browser --bundle --input-exe <path-to-pkg-binary-or-symlink> --output-directory public/ [--gc-threshold=512]
      $ #        ^ target  ^ one-file JS  ^ binary loc                          ^ where to spit out wasm   ^ optionally increase memory limit beyond default 64MB
      
    5. If targeting browser, include the generated entrypoint <pkg-name>.js in your HTML source. This will load the WASM for you and get it running, which takes some time and has no event corresponding to its completion.

  2. Compiling Haskell sources straight: This enables you to export Haskell functions via the foreign export syntax. In my opinion, this is not very well-documented by the dev team, so this may set some records straight.

    1. Run this base ahc-link command first to generate boilerplate:

      $ ahc-link --browser --bundle --input-hs <Main-or-whatever-entrypoint>.hs --output-directory public/
      $          ^ target  ^ minify ^ entrypoint file                           ^ where to spit out WASM
      
    2. Edit <entrypoint-name>.mjs. This is where the "Asterius instance" is initialized and the only place it's meaningfully exposed — this instance manages all the imports/exports. The default file looks something like this (minus the // * which I put there for emphasis):

      import * as rts from "./rts.mjs";
      import module from "./sandbox.wasm.mjs";
      import req from "./sandbox.req.mjs";
      module.then(m => rts.newAsteriusInstance(Object.assign(req, {module: m}))).then(i => {
        i.exports.main().catch(err => {if (!(err.startsWith('ExitSuccess') || err.startsWith('ExitFailure '))) i.fs.writeSync(2, `sandbox: ${err}
      `)});
        // *
      });
      

      The instance is contained in variable i at // *. The exports are located as functions in i.exports: note the main call happens here.

    3. Recompile, this time with a flag --input-mjs public/<entrypoint-name>.mjs to let ahc-link know to not clobber your custom mjs file and instead minify with it, as well as -export-function=<fn-name> for every function you export:

      $ ahc-link --browser --bundle --input-mjs public/<entrypoint-name>.mjs --input-hs <Main-or-whatever-entrypoint>.hs --export-function=<fn-name> --output-directory public/
      $          ^ target  ^ minify ^ customized mjs file                    ^ entrypoint file                           ^ one [of many] exported functions  ^ where to spit out WASM
      
    4. If targeting browser, include the generated entrypoint <pkg-name>.js in your HTML source.

To repeat for emphasis: there is no event [yet] to tell you when the WASM has loaded, so exported functions and functionality in the WASM should be polled or treated as conditionally null.

Here are two Asterius Github issues by yours truly about:

  1. The fact that ahc-cabal doesn't let you export functions;
  2. The fact that closures above the --gc-threshold sometimes disappear