Skip to content

GHC JS browser introduction blog post/tutorial #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e5aef39
WIP blog post & associated example code for GHC JS browser example
JoshMeredith Dec 19, 2022
e8624e2
WIP browser example blog post - building ghc and start of node/browse…
JoshMeredith Dec 20, 2022
ccbd4e7
Browser example blog post
JoshMeredith Dec 21, 2022
031eedc
Browser example blog post intro/conclusion
JoshMeredith Dec 22, 2022
d3d4e51
Browser example: incorporate feedback
JoshMeredith Jan 3, 2023
8027fcd
Browser example: incorporate feedback 2
JoshMeredith Jan 10, 2023
69d0b35
miscellaneous wordsmithing
Jan 12, 2023
34a523e
add code explanation
Jan 12, 2023
2bdd961
more wordsmithing
Jan 12, 2023
edfce60
Update blog/ghc-js-browser-example/browser-example.md
JoshMeredith Jan 18, 2023
35268e9
Update browser-example.md
JoshMeredith Jan 18, 2023
f99717a
Browser example: screenshot and other small changes
JoshMeredith Jan 18, 2023
15f1a0a
Browser example: typo
JoshMeredith Jan 18, 2023
6777696
Update blog/2023-01-18-javascript-browser-tutorial.md
JoshMeredith Jan 19, 2023
01a3554
Update 2023-01-18-javascript-browser-tutorial.md
JoshMeredith Jan 19, 2023
cfcec31
Update 2023-01-18-javascript-browser-tutorial.md
JoshMeredith Jan 19, 2023
1d24d15
Update 2023-01-18-javascript-browser-tutorial.md
JoshMeredith Jan 19, 2023
571344e
Set docasaurus static directory
JoshMeredith Jan 19, 2023
49d1f42
fix en-dashes, remove passive voices
Jan 20, 2023
8d6ac89
Update 2023-01-18-javascript-browser-tutorial.md
JoshMeredith Jan 20, 2023
2a8a8a0
Update blog/2023-01-18-javascript-browser-tutorial.md
hsyl20 Jan 20, 2023
97373b1
Update blog/2023-01-18-javascript-browser-tutorial.md
hsyl20 Jan 20, 2023
524bcb0
Update blog/2023-01-18-javascript-browser-tutorial.md
hsyl20 Jan 20, 2023
444ca9b
Update blog/2023-01-18-javascript-browser-tutorial.md
hsyl20 Jan 20, 2023
f6bcd1e
Update 2023-01-18-javascript-browser-tutorial.md
JoshMeredith Jan 20, 2023
0db79fd
Update 2023-01-18-javascript-browser-tutorial.md
JoshMeredith Jan 23, 2023
1a560c1
Update blog/2023-01-18-javascript-browser-tutorial.md
hsyl20 Jan 24, 2023
8dd58a9
Update and rename 2023-01-18-javascript-browser-tutorial.md to 2023-0…
hsyl20 Jan 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 293 additions & 0 deletions blog/2023-01-24-javascript-browser-tutorial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
---
slug: 2023-01-24-javascript-browser-tutorial
title: Using GHC's JavaScript Backend in the Browser
authors: [sylvain, doyougnu, luite, josh]
tags: [ghc, javascript, cross-compilation]
---


# Using GHC's JavaScript Backend in the Browser

In a previous
[post](https://engineering.iog.io/2022-12-13-ghc-js-backend-merged) we
introduced GHC's new JavaScript backend, which allows the compilation of Haskell
code into JavaScript. This is the first tutorial in a new series about the
JavaScript backend. We plan to write more of those in the coming weeks and
months as we add new features (e.g. support for "foreign exports" that will
allow JavaScript code to call into Haskell code, support for Template Haskell,
etc.). For now it relies on our "insider" knowledge (e.g. how the FFI works)
that isn't well documented elsewhere. We do plan to add a chapter about the
JavaScript backend in GHC's user guide, but for now your best chance is to look
at GHCJS's documentation or at the source code. In this post, we'll build GHC as
a JavaScript cross-compiler and run a trivial Haskell program in the browser.

Please note: this is a technology preview of the in-development JavaScript backend
for GHC. Not all Haskell features are implemented, and bugs are expected. It is
currently rather complicated for JavaScript code to call into Haskell code ("foreign
exports" aren't implemented). GHC isn't a multi-target compiler yet, so a GHC executable
built for a native platform (Linux/x86-64, Windows/x86-64, Darwin/AArch64...) as currently distributed (via ghcup, Stack, binary distributions, etc.) won't be able to produce JavaScript. Official prebuilt binary distributions are likely to remain
unavailable until GHC gains multi-target support - requiring the JavaScript backend
to be built from source even after the backend matures.
That's why we start this post with the required steps to build yourself
a GHC compiler capable of producing JavaScript.

## Building GHC as a Cross Compiler to JavaScript

### Installing Dependencies

First we need to install all the typical dependencies for GHC plus `Emscripten`,
so our final list is:

* GHC version 9.2 or later
* Cabal
* Alex
* Happy
* Emscripten to configure with
* (Optional) NodeJS to run JavaScript locally


Let's take these in order, a standard GHC distribution with Cabal is needed so we can boot our new compiler.
We recommend using GHCUP
([https://www.haskell.org/ghcup/install/](https://www.haskell.org/ghcup/install/)),
or your system's package manager to install the this.

We need Alex and Happy to build GHC, these can be installed through Cabal:

```
cabal install alex happy -j
```

We need Emscripten during the `configure` step of the build. `Emscripten` should be available in most package managers, but you can also build and install it from source:

```
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
```

After installing Emscripten, `emconfigure` should be available on your system
path. Use `which emconfigure` to check that it is on your `$PATH`. If you built
from source, then the output should point to a location within the emsdk git
project like so:

```
$ which emconfigure
/path/to/emsdk/upstream/emscripten/emconfigure
```

For more detailed installation instructions, see [https://emscripten.org/docs/getting_started/downloads.html](https://emscripten.org/docs/getting_started/downloads.html).

That's all we need to build GHC as a cross compiler. NodeJS can be installed via your system's package manager if you want to run the JavaScript programs locally. We'll assume it's in your `$PATH` for the rest of the blog post.


### Building GHC

With all the dependencies installed, we can clone GHC HEAD and build the cross compiler:
```
git clone https://gitlab.haskell.org/ghc/ghc.git --recursive
```
You should notice quite a few submodules being cloned as well as the main repo; expect this to take a while. Once this has completed, navigate to the `ghc` directory and run the following configuration commands:
```
cd ghc
./boot
emconfigure ./configure --target=js-unknown-ghcjs
```

`emconfigure ./configure --target=js-unknown-ghcjs` will finish by outputting a screen that looks like:
```
----------------------------------------------------------------------
Configure completed successfully.

Building GHC version : 9.5.20221219
Git commit id : 761c1f49f55afc9a9f290fafb48885c2033069ed

Build platform : x86_64-unknown-linux
Host platform : x86_64-unknown-linux
Target platform : js-unknown-ghcjs

Bootstrapping using : /home/josh/.ghcup/bin/ghc
which is version : 9.4.2
with threaded RTS? : YES

Using (for bootstrapping) : gcc
Using clang : /home/josh/emsdk/upstream/emscripten/emcc
which is version : 15.0.0
linker options :
Building a cross compiler : YES
Unregisterised : NO
TablesNextToCode : YES
Build GMP in tree : NO
hs-cpp : /home/josh/emsdk/upstream/emscripten/emcc
hs-cpp-flags : -E -undef -traditional -Wno-invalid-pp-token -Wno-unicode -Wno-trigraphs
ar : /home/josh/emsdk/upstream/emscripten/emar
ld : /home/josh/emsdk/upstream/emscripten/emcc
nm : /home/josh/emsdk/upstream/bin/llvm-nm
objdump : /usr/bin/objdump
ranlib : /home/josh/emsdk/upstream/emscripten/emranlib
otool : otool
install_name_tool : install_name_tool
windres :
dllwrap :
genlib :
Happy : /home/josh/.cabal/bin/happy (1.20.0)
Alex : /home/josh/.cabal/bin/alex (3.2.7.1)
sphinx-build :
xelatex :
makeinfo :
git : /usr/bin/git
cabal-install : /home/josh/.cabal/bin/cabal

Using LLVM tools
clang : clang
llc : llc-14
opt : opt-14

HsColour was not found; documentation will not contain source links

Tools to build Sphinx HTML documentation available: NO
Tools to build Sphinx PDF documentation available: NO
Tools to build Sphinx INFO documentation available: NO
----------------------------------------------------------------------
```

If everything is correct, you'll see that the `Target platform` is set to
`js-unknown-ghcjs`, and the build tools will be set to their
Emscripten counterparts: `ar` becomes `emar`, `nm` becomes `llvm-nm`, etc.

Finally, to build GHC:
```
./hadrian/build --bignum=native -j --docs=none
```

Expect this to take around a half hour or longer. If all goes well you should see:
```
/--------------------------------------------------------\
| Successfully built library 'ghc' (Stage1, way p). |
| Library: _build/stage1/compiler/build/libHSghc-9.5_p.a |
| Library synopsis: The GHC API. |
\--------------------------------------------------------/
| Copy package 'ghc'
# cabal-copy (for _build/stage1/lib/package.conf.d/ghc-9.5.conf)
| Run GhcPkg Recache Stage1: none => none
| Copy file: _build/stage0/bin/js-unknown-ghcjs-ghc => _build/stage1/bin/js-unknown-ghcjs-ghc
Build completed in 1h00m
```

Take note of `_build/stage1/bin/js-unknown-ghcjs-ghc` path. This is the GHC executable that we'll use to compile to JavaScript. To make life easier on ourselves we can alias it:

```
alias ghc-js=`pwd`/_build/stage1/bin/js-unknown-ghcjs-ghc
```

## First Haskell to JavaScript Program

Now that we have a version of GHC that can output JavaScript, let's compile a Haskell program and run it with NodeJS. Make a file named "HelloJS.hs", with the following contents:

```haskell
-- HelloJS.hs
module Main where

main :: IO ()
main = putStrLn "Hello, JavaScript!"
```

Now we can compile it using the alias we defined earlier:
```
ghc-js HelloJS.hs
```

You should see the following output, and a `HelloJS` executable.

```
[1 of 2] Compiling Main ( HelloJS.hs, HelloJS.o )
[2 of 2] Linking HelloJS.jsexe
```

If you have NodeJS is on your `Path`, then this executable can be run just like any other command line program:

```
./HelloJS
Hello, JavaScript!
```

Notice that a directory called `HelloJS.jsexe` was created. This directory
contains all the final JavaScript code, including a file named `all.js`, and a
minimal `index.html` HTML file that wraps `all.js`. For now, we'll only care
about `all.js` and return to `index.html later. `all.js` _is_ the payload of our
`HelloJS` exectuable. The executable is simply a copy of `all.js`, with a call
to `node` added to the top. We could have equivalently run our program with:

```
node HelloJS.jsexe/all.js
```

## Haskell in the Browser

We saw in the previous example that GHC's JavaScript backend allows us to write
Haskell and run the output JavaScript with NodeJS. This produces a portable
executable, but otherwise doesn't enable anything we couldn't do before; GHC can
already compile Haskell to run on most platforms! So let's do something novel,
and run Haskell in the browser.

In this example, we'll use Haskell to draw a simple SVG circle to our browser window. Put the following code in a file named `HelloBrowser.hs`:

```haskell
-- HelloBrowser.hs
module Main where

import Foreign.C.String

foreign import javascript "((arr,offset) => document.body.innerHTML = h$decodeUtf8z(arr,offset))"
setInnerHtml :: CString -> IO ()

circle :: String
circle = "<svg width=300 height=300><circle cx=50% cy=50% r=50%></circle></svg>"

main :: IO ()
main = withCString circle setInnerHtml
```

Notice that we've encountered a Haskell feature that's only available in the
JavaScript backend: JavaScript foreign imports. This feature allows our Haskell
program to call JavaScript functions. In our example we use this feature to call
a JavaScript [arrow
function](https://262.ecma-international.org/13.0/#prod-ArrowFunction) that
updates the `body` of the page with our HTML snippet containing a drawing of a
circle. Alternatively, we could have set the foreign import to a function symbol
like so:

```
foreign import javascript "setInnerHTML"
setInnerHtml :: CString -> IO ()
```

where `setInnerHTML` is defined in a `.js` file that is then loaded
by passing the JavaScript file to GHC along with the Haskell sources.

Next, we can compile our program to JavaScript, again with our built GHC:

```
ghc-js HelloBrowser.hs
```

Or `ghc-js HelloBrowser.hs foo.js` if `setInnerHTML` is defined in `foo.js`.


Recall the `index.html` file inside the `HelloBrowser.jsexe` directory. This HTML
file has our compiled JavaScript already included, so if you open it in your
browser, you'll find it loads our SVG circle in the top-left of the page!

![Example webpage screenshot](/img/browser-screenshot.png)

`index.html` contains the minimal HTML code required to load the generated JavaScript code. It simply loads the `all.js` file mentioned above with the following `script` tag that you can reuse in your own HTML files:
```html
<script language="javascript" src="all.js" defer></script>
```

As the JS backend still lacks support for some FFI features (foreign exports, foreign "wrapper" imports...), JavaScript codes can't easily interact with Haskell codes. It reduces the amount of advanced/interesting examples we can present for now. We'll publish new blog posts illustrating these features when they will be implemented.

## Conclusion

In this post, we've seen how to build a first Haskell program to run in the browser using a preview of GHC's in-development JavaScript backend. This program used "foreign imports" to make a JavaScript function available within the Haskell code, which allows a limited interaction between Haskell and the browser. We also saw the structure of the outputs of the JavaScript backend, in the `.jsexe` directory, and how this allows our Haskell program to be invoked by a custom HTML wrapper. This was all enabled by building a version of GHC from source, with the build process having been configured with Emscripten to produce a GHC exectuable that targets JavaScript.
1 change: 1 addition & 0 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const config = {
favicon: 'img/favicon.ico',
organizationName: 'input-output-hk', // Usually your GitHub org/user name.
projectName: 'engineering', // Usually your repo name.
staticDirectories: ['static'],

presets: [
[
Expand Down
Binary file added static/img/browser-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.