Run JSaddle JSM actions with the GHC Wasm backend.
This can for example be used to compile and run Miso or Reflex apps in the browser.
Important
This project is in an early stage.
-
Miso examples: https://github.com/tweag/ghc-wasm-miso-examples
-
Reflex examples: https://github.com/tweag/ghc-wasm-reflex-examples
-
Ormolu Live: https://github.com/tweag/ormolu/tree/master/ormolu-live (uses the web worker approach described below)
Install a Wasm-enabled GHC with support for the Wasm JSFFI (including synchronous JSFFI exports1) from ghc-wasm-meta (GHC 9.10 or newer).
Assuming you built your application as an app :: JSM ():
import Language.Javascript.JSaddle.Wasm qualified as JSaddle.Wasm
foreign export javascript "hs_start" main :: IO ()
main :: IO ()
main = JSaddle.Wasm.run appBuild the Wasm binary with the following GHC options:
ghc-options: -no-hs-main -optl-mexec-model=reactor "-optl-Wl,--export=hs_start"Now, run the post-linker script as described in the GHC User's Guide; we will call the resulting JavaScript file ghc_wasm_jsffi.js.
Then, following the GHC User's Guide, you can run the Wasm binary in the browser via e.g. browser_wasi_shim:
import { WASI, OpenFile, File, ConsoleStdout } from "@bjorn3/browser_wasi_shim";
import ghc_wasm_jsffi from "./ghc_wasm_jsffi.js";
const fds = [
new OpenFile(new File([])), // stdin
ConsoleStdout.lineBuffered((msg) => console.log(`[WASI stdout] ${msg}`)),
ConsoleStdout.lineBuffered((msg) => console.warn(`[WASI stderr] ${msg}`)),
];
const options = { debug: false };
const wasi = new WASI([], [], fds, options);
const instance_exports = {};
const { instance } = await WebAssembly.instantiateStreaming(fetch("app.wasm"), {
wasi_snapshot_preview1: wasi.wasiImport,
ghc_wasm_jsffi: ghc_wasm_jsffi(instance_exports),
});
Object.assign(instance_exports, instance.exports);
wasi.initialize(instance);
await instance.exports.hs_start();It is also possible to run the Wasm worker in a different execution environment (e.g. a web worker) than the JSaddle JavaScript code that dispatches the JSaddle command messages.
An advantage of this approach is that computationally expensive operations in Wasm do not block the UI thread. A disadvantage is that there is some overhead for copying the data back and forth, and everything relying on synchronous callbacks (e.g. stopPropagation/preventDefault) definitely no longer works.
-
Instead of the
runfunction above, you need to userunWorker(again assumingapp :: JSM ()):import Language.Javascript.JSaddle.Wasm qualified as JSaddle.Wasm foreign export javascript "hs_runWorker" runWorker :: JSVal -> IO () runWorker :: JSVal -> IO () runWorker = JSaddle.Wasm.runWorker app
The argument to
runWorkerhere can be any message port in the sense of the Channel Messaging API. In particular, it must provide apostMessagefunction and amessageevent.For example, in a web worker, you can initialize the Wasm module as above, and then run
await instance.exports.hs_runWorker(globalThis);
as
globalThis(orself) in a web worker is a message port. -
Additionally, you need to run the JSaddle command dispatching logic on the other end of the message port.
The necessary chunk of JavaScript is available as
jsaddleScriptboth fromLanguage.Javascript.JSaddle.Wasmfrom the main library, and also fromLanguage.Javascript.JSaddle.Wasm.JSfrom thejspublic sublibrary, where the latter has the advantage to not depend on any JSFFI, so you can build a normal WASI command module or even a native executable while still depending on it.It provides a function
runJSaddletaking a single argument, a message port.One way to invoke it is to save
jsaddleScriptto some file, include it via ascripttag in your HTML file, and then runconst worker = new Worker("my-worker.js"); runJSaddle(worker);
Additionally, when other packages use the Language.Javascript.JSaddle.Wasm.TH module, you need to disable the eval-via-jsffi flag, e.g. by adding the following to your cabal.project:
package jsaddle-wasm
flags: -eval-via-jsffi- Testing (e.g. via Selenium).
- Add logging/stats.
- Performance/benchmarking (not clear that this is actually a bottleneck for most applications).
-
Optimize existing command-based implementation.
- Reuse buffers
- Use a serialization format more efficient than JSON.
-
Patch
jsaddleto not go through commands, by using the Wasm JS FFI. -
Implement
ghcjs-domAPI directly via the Wasm JS FFI.This would involve creating a
ghcjs-dom-wasmpackage by adapting the FFI import syntax fromghcjs-dom-jsffi/ghcjs-dom-javascriptappropriately.Currently, the generic
ghcjs-dom-jsaddleseems to work fine, so it seems sensible to wait with this until benchmarks or other concerns motivate this.
-
- WebGHC/jsaddle-wasm for the analogue for WebGHC instead of the GHC Wasm backend.
Footnotes
-
Otherwise, you will see errors involving
unknown type name 'HsFUN'. ↩