|
| 1 | +--- |
| 2 | +slug: 2023-01-24-javascript-browser-tutorial |
| 3 | +title: Using GHC's JavaScript Backend in the Browser |
| 4 | +authors: [sylvain, doyougnu, luite, josh] |
| 5 | +tags: [ghc, javascript, cross-compilation] |
| 6 | +--- |
| 7 | + |
| 8 | + |
| 9 | +# Using GHC's JavaScript Backend in the Browser |
| 10 | + |
| 11 | +In a previous |
| 12 | +[post](https://engineering.iog.io/2022-12-13-ghc-js-backend-merged) we |
| 13 | +introduced GHC's new JavaScript backend, which allows the compilation of Haskell |
| 14 | +code into JavaScript. This is the first tutorial in a new series about the |
| 15 | +JavaScript backend. We plan to write more of those in the coming weeks and |
| 16 | +months as we add new features (e.g. support for "foreign exports" that will |
| 17 | +allow JavaScript code to call into Haskell code, support for Template Haskell, |
| 18 | +etc.). For now it relies on our "insider" knowledge (e.g. how the FFI works) |
| 19 | +that isn't well documented elsewhere. We do plan to add a chapter about the |
| 20 | +JavaScript backend in GHC's user guide, but for now your best chance is to look |
| 21 | +at GHCJS's documentation or at the source code. In this post, we'll build GHC as |
| 22 | +a JavaScript cross-compiler and run a trivial Haskell program in the browser. |
| 23 | + |
| 24 | +Please note: this is a technology preview of the in-development JavaScript backend |
| 25 | +for GHC. Not all Haskell features are implemented, and bugs are expected. It is |
| 26 | +currently rather complicated for JavaScript code to call into Haskell code ("foreign |
| 27 | +exports" aren't implemented). GHC isn't a multi-target compiler yet, so a GHC executable |
| 28 | +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 |
| 29 | +unavailable until GHC gains multi-target support - requiring the JavaScript backend |
| 30 | +to be built from source even after the backend matures. |
| 31 | +That's why we start this post with the required steps to build yourself |
| 32 | +a GHC compiler capable of producing JavaScript. |
| 33 | + |
| 34 | +## Building GHC as a Cross Compiler to JavaScript |
| 35 | + |
| 36 | +### Installing Dependencies |
| 37 | + |
| 38 | +First we need to install all the typical dependencies for GHC plus `Emscripten`, |
| 39 | +so our final list is: |
| 40 | + |
| 41 | +* GHC version 9.2 or later |
| 42 | +* Cabal |
| 43 | +* Alex |
| 44 | +* Happy |
| 45 | +* Emscripten to configure with |
| 46 | +* (Optional) NodeJS to run JavaScript locally |
| 47 | + |
| 48 | + |
| 49 | +Let's take these in order, a standard GHC distribution with Cabal is needed so we can boot our new compiler. |
| 50 | +We recommend using GHCUP |
| 51 | +([https://www.haskell.org/ghcup/install/](https://www.haskell.org/ghcup/install/)), |
| 52 | +or your system's package manager to install the this. |
| 53 | + |
| 54 | +We need Alex and Happy to build GHC, these can be installed through Cabal: |
| 55 | + |
| 56 | +``` |
| 57 | +cabal install alex happy -j |
| 58 | +``` |
| 59 | + |
| 60 | +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: |
| 61 | + |
| 62 | +``` |
| 63 | +git clone https://github.com/emscripten-core/emsdk.git |
| 64 | +cd emsdk |
| 65 | +./emsdk install latest |
| 66 | +./emsdk activate latest |
| 67 | +source ./emsdk_env.sh |
| 68 | +``` |
| 69 | + |
| 70 | +After installing Emscripten, `emconfigure` should be available on your system |
| 71 | +path. Use `which emconfigure` to check that it is on your `$PATH`. If you built |
| 72 | +from source, then the output should point to a location within the emsdk git |
| 73 | +project like so: |
| 74 | + |
| 75 | +``` |
| 76 | +$ which emconfigure |
| 77 | +/path/to/emsdk/upstream/emscripten/emconfigure |
| 78 | +``` |
| 79 | + |
| 80 | +For more detailed installation instructions, see [https://emscripten.org/docs/getting_started/downloads.html](https://emscripten.org/docs/getting_started/downloads.html). |
| 81 | + |
| 82 | +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. |
| 83 | + |
| 84 | + |
| 85 | +### Building GHC |
| 86 | + |
| 87 | +With all the dependencies installed, we can clone GHC HEAD and build the cross compiler: |
| 88 | +``` |
| 89 | +git clone https://gitlab.haskell.org/ghc/ghc.git --recursive |
| 90 | +``` |
| 91 | +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: |
| 92 | +``` |
| 93 | +cd ghc |
| 94 | +./boot |
| 95 | +emconfigure ./configure --target=js-unknown-ghcjs |
| 96 | +``` |
| 97 | + |
| 98 | +`emconfigure ./configure --target=js-unknown-ghcjs` will finish by outputting a screen that looks like: |
| 99 | +``` |
| 100 | +---------------------------------------------------------------------- |
| 101 | +Configure completed successfully. |
| 102 | +
|
| 103 | + Building GHC version : 9.5.20221219 |
| 104 | + Git commit id : 761c1f49f55afc9a9f290fafb48885c2033069ed |
| 105 | +
|
| 106 | + Build platform : x86_64-unknown-linux |
| 107 | + Host platform : x86_64-unknown-linux |
| 108 | + Target platform : js-unknown-ghcjs |
| 109 | +
|
| 110 | + Bootstrapping using : /home/josh/.ghcup/bin/ghc |
| 111 | + which is version : 9.4.2 |
| 112 | + with threaded RTS? : YES |
| 113 | +
|
| 114 | + Using (for bootstrapping) : gcc |
| 115 | + Using clang : /home/josh/emsdk/upstream/emscripten/emcc |
| 116 | + which is version : 15.0.0 |
| 117 | + linker options : |
| 118 | + Building a cross compiler : YES |
| 119 | + Unregisterised : NO |
| 120 | + TablesNextToCode : YES |
| 121 | + Build GMP in tree : NO |
| 122 | + hs-cpp : /home/josh/emsdk/upstream/emscripten/emcc |
| 123 | + hs-cpp-flags : -E -undef -traditional -Wno-invalid-pp-token -Wno-unicode -Wno-trigraphs |
| 124 | + ar : /home/josh/emsdk/upstream/emscripten/emar |
| 125 | + ld : /home/josh/emsdk/upstream/emscripten/emcc |
| 126 | + nm : /home/josh/emsdk/upstream/bin/llvm-nm |
| 127 | + objdump : /usr/bin/objdump |
| 128 | + ranlib : /home/josh/emsdk/upstream/emscripten/emranlib |
| 129 | + otool : otool |
| 130 | + install_name_tool : install_name_tool |
| 131 | + windres : |
| 132 | + dllwrap : |
| 133 | + genlib : |
| 134 | + Happy : /home/josh/.cabal/bin/happy (1.20.0) |
| 135 | + Alex : /home/josh/.cabal/bin/alex (3.2.7.1) |
| 136 | + sphinx-build : |
| 137 | + xelatex : |
| 138 | + makeinfo : |
| 139 | + git : /usr/bin/git |
| 140 | + cabal-install : /home/josh/.cabal/bin/cabal |
| 141 | +
|
| 142 | + Using LLVM tools |
| 143 | + clang : clang |
| 144 | + llc : llc-14 |
| 145 | + opt : opt-14 |
| 146 | +
|
| 147 | + HsColour was not found; documentation will not contain source links |
| 148 | +
|
| 149 | + Tools to build Sphinx HTML documentation available: NO |
| 150 | + Tools to build Sphinx PDF documentation available: NO |
| 151 | + Tools to build Sphinx INFO documentation available: NO |
| 152 | +---------------------------------------------------------------------- |
| 153 | +``` |
| 154 | + |
| 155 | +If everything is correct, you'll see that the `Target platform` is set to |
| 156 | +`js-unknown-ghcjs`, and the build tools will be set to their |
| 157 | +Emscripten counterparts: `ar` becomes `emar`, `nm` becomes `llvm-nm`, etc. |
| 158 | + |
| 159 | +Finally, to build GHC: |
| 160 | +``` |
| 161 | +./hadrian/build --bignum=native -j --docs=none |
| 162 | +``` |
| 163 | + |
| 164 | +Expect this to take around a half hour or longer. If all goes well you should see: |
| 165 | +``` |
| 166 | +/--------------------------------------------------------\ |
| 167 | +| Successfully built library 'ghc' (Stage1, way p). | |
| 168 | +| Library: _build/stage1/compiler/build/libHSghc-9.5_p.a | |
| 169 | +| Library synopsis: The GHC API. | |
| 170 | +\--------------------------------------------------------/ |
| 171 | +| Copy package 'ghc' |
| 172 | +# cabal-copy (for _build/stage1/lib/package.conf.d/ghc-9.5.conf) |
| 173 | +| Run GhcPkg Recache Stage1: none => none |
| 174 | +| Copy file: _build/stage0/bin/js-unknown-ghcjs-ghc => _build/stage1/bin/js-unknown-ghcjs-ghc |
| 175 | +Build completed in 1h00m |
| 176 | +``` |
| 177 | + |
| 178 | +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: |
| 179 | + |
| 180 | +``` |
| 181 | +alias ghc-js=`pwd`/_build/stage1/bin/js-unknown-ghcjs-ghc |
| 182 | +``` |
| 183 | + |
| 184 | +## First Haskell to JavaScript Program |
| 185 | + |
| 186 | +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: |
| 187 | + |
| 188 | +```haskell |
| 189 | +-- HelloJS.hs |
| 190 | +module Main where |
| 191 | + |
| 192 | +main :: IO () |
| 193 | +main = putStrLn "Hello, JavaScript!" |
| 194 | +``` |
| 195 | + |
| 196 | +Now we can compile it using the alias we defined earlier: |
| 197 | +``` |
| 198 | +ghc-js HelloJS.hs |
| 199 | +``` |
| 200 | + |
| 201 | +You should see the following output, and a `HelloJS` executable. |
| 202 | + |
| 203 | +``` |
| 204 | +[1 of 2] Compiling Main ( HelloJS.hs, HelloJS.o ) |
| 205 | +[2 of 2] Linking HelloJS.jsexe |
| 206 | +``` |
| 207 | + |
| 208 | +If you have NodeJS is on your `Path`, then this executable can be run just like any other command line program: |
| 209 | + |
| 210 | +``` |
| 211 | +./HelloJS |
| 212 | +Hello, JavaScript! |
| 213 | +``` |
| 214 | + |
| 215 | +Notice that a directory called `HelloJS.jsexe` was created. This directory |
| 216 | +contains all the final JavaScript code, including a file named `all.js`, and a |
| 217 | +minimal `index.html` HTML file that wraps `all.js`. For now, we'll only care |
| 218 | +about `all.js` and return to `index.html later. `all.js` _is_ the payload of our |
| 219 | +`HelloJS` exectuable. The executable is simply a copy of `all.js`, with a call |
| 220 | +to `node` added to the top. We could have equivalently run our program with: |
| 221 | + |
| 222 | +``` |
| 223 | +node HelloJS.jsexe/all.js |
| 224 | +``` |
| 225 | + |
| 226 | +## Haskell in the Browser |
| 227 | + |
| 228 | +We saw in the previous example that GHC's JavaScript backend allows us to write |
| 229 | +Haskell and run the output JavaScript with NodeJS. This produces a portable |
| 230 | +executable, but otherwise doesn't enable anything we couldn't do before; GHC can |
| 231 | +already compile Haskell to run on most platforms! So let's do something novel, |
| 232 | +and run Haskell in the browser. |
| 233 | + |
| 234 | +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`: |
| 235 | + |
| 236 | +```haskell |
| 237 | +-- HelloBrowser.hs |
| 238 | +module Main where |
| 239 | + |
| 240 | +import Foreign.C.String |
| 241 | + |
| 242 | +foreign import javascript "((arr,offset) => document.body.innerHTML = h$decodeUtf8z(arr,offset))" |
| 243 | + setInnerHtml :: CString -> IO () |
| 244 | + |
| 245 | +circle :: String |
| 246 | +circle = "<svg width=300 height=300><circle cx=50% cy=50% r=50%></circle></svg>" |
| 247 | + |
| 248 | +main :: IO () |
| 249 | +main = withCString circle setInnerHtml |
| 250 | +``` |
| 251 | + |
| 252 | +Notice that we've encountered a Haskell feature that's only available in the |
| 253 | +JavaScript backend: JavaScript foreign imports. This feature allows our Haskell |
| 254 | +program to call JavaScript functions. In our example we use this feature to call |
| 255 | +a JavaScript [arrow |
| 256 | +function](https://262.ecma-international.org/13.0/#prod-ArrowFunction) that |
| 257 | +updates the `body` of the page with our HTML snippet containing a drawing of a |
| 258 | +circle. Alternatively, we could have set the foreign import to a function symbol |
| 259 | +like so: |
| 260 | + |
| 261 | +``` |
| 262 | +foreign import javascript "setInnerHTML" |
| 263 | + setInnerHtml :: CString -> IO () |
| 264 | +``` |
| 265 | + |
| 266 | +where `setInnerHTML` is defined in a `.js` file that is then loaded |
| 267 | +by passing the JavaScript file to GHC along with the Haskell sources. |
| 268 | + |
| 269 | +Next, we can compile our program to JavaScript, again with our built GHC: |
| 270 | + |
| 271 | +``` |
| 272 | +ghc-js HelloBrowser.hs |
| 273 | +``` |
| 274 | + |
| 275 | +Or `ghc-js HelloBrowser.hs foo.js` if `setInnerHTML` is defined in `foo.js`. |
| 276 | + |
| 277 | + |
| 278 | +Recall the `index.html` file inside the `HelloBrowser.jsexe` directory. This HTML |
| 279 | +file has our compiled JavaScript already included, so if you open it in your |
| 280 | +browser, you'll find it loads our SVG circle in the top-left of the page! |
| 281 | + |
| 282 | + |
| 283 | + |
| 284 | +`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: |
| 285 | +```html |
| 286 | +<script language="javascript" src="all.js" defer></script> |
| 287 | +``` |
| 288 | + |
| 289 | +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. |
| 290 | + |
| 291 | +## Conclusion |
| 292 | + |
| 293 | +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. |
0 commit comments