Skip to content

Commit 59ac730

Browse files
JoshMeredithdoyougnuhsyl20
authored
GHC JS browser introduction blog post/tutorial (#24)
* WIP blog post & associated example code for GHC JS browser example * WIP browser example blog post - building ghc and start of node/browser examples * Browser example blog post * Browser example blog post intro/conclusion * Browser example: incorporate feedback * Browser example: incorporate feedback 2 * miscellaneous wordsmithing * add code explanation * more wordsmithing * Update blog/ghc-js-browser-example/browser-example.md Co-authored-by: Jeffrey Young <[email protected]> * Update browser-example.md * Browser example: screenshot and other small changes * Browser example: typo * Update blog/2023-01-18-javascript-browser-tutorial.md Co-authored-by: Sylvain Henry <[email protected]> * Update 2023-01-18-javascript-browser-tutorial.md * Update 2023-01-18-javascript-browser-tutorial.md * Update 2023-01-18-javascript-browser-tutorial.md * Set docasaurus static directory * fix en-dashes, remove passive voices * Update 2023-01-18-javascript-browser-tutorial.md * Update blog/2023-01-18-javascript-browser-tutorial.md * Update blog/2023-01-18-javascript-browser-tutorial.md * Update blog/2023-01-18-javascript-browser-tutorial.md * Update blog/2023-01-18-javascript-browser-tutorial.md * Update 2023-01-18-javascript-browser-tutorial.md * Update 2023-01-18-javascript-browser-tutorial.md * Update blog/2023-01-18-javascript-browser-tutorial.md * Update and rename 2023-01-18-javascript-browser-tutorial.md to 2023-01-24-javascript-browser-tutorial.md Co-authored-by: doyougnu <[email protected]> Co-authored-by: Sylvain Henry <[email protected]>
1 parent 1b8883e commit 59ac730

File tree

3 files changed

+294
-0
lines changed

3 files changed

+294
-0
lines changed
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
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+
![Example webpage screenshot](/img/browser-screenshot.png)
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.

docusaurus.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const config = {
1515
favicon: 'img/favicon.ico',
1616
organizationName: 'input-output-hk', // Usually your GitHub org/user name.
1717
projectName: 'engineering', // Usually your repo name.
18+
staticDirectories: ['static'],
1819

1920
presets: [
2021
[

static/img/browser-screenshot.png

6.75 KB
Loading

0 commit comments

Comments
 (0)