aboutsummaryrefslogtreecommitdiff
path: root/src/blog/2025-06-06-webdev/webdev.md
diff options
context:
space:
mode:
authorSebastiano Tronto <sebastiano@tronto.net>2025-09-28 09:34:46 +0200
committerSebastiano Tronto <sebastiano@tronto.net>2025-09-28 09:34:46 +0200
commitaa42ae52f6d5fc32727ab4c8a9d8375f9247e7c8 (patch)
tree222ae7cc52bf8395d44324bb829e2686acde10d8 /src/blog/2025-06-06-webdev/webdev.md
parentb55c05b91316f2054b3046794799e7eaa5ffcd8d (diff)
parentc192f8cde924377ffe4085a80f2baeb525658af1 (diff)
downloadsebastiano.tronto.net-aa42ae52f6d5fc32727ab4c8a9d8375f9247e7c8.tar.gz
sebastiano.tronto.net-aa42ae52f6d5fc32727ab4c8a9d8375f9247e7c8.zip
Merge branch 'master' of tronto.net:sebastiano.tronto.net
Diffstat (limited to 'src/blog/2025-06-06-webdev/webdev.md')
-rw-r--r--src/blog/2025-06-06-webdev/webdev.md1186
1 files changed, 1186 insertions, 0 deletions
diff --git a/src/blog/2025-06-06-webdev/webdev.md b/src/blog/2025-06-06-webdev/webdev.md
new file mode 100644
index 0000000..6b049e5
--- /dev/null
+++ b/src/blog/2025-06-06-webdev/webdev.md
@@ -0,0 +1,1186 @@
1# A masochist's guide to web development
2
3## Table of contents
4
5* [Introduction](#introduction)
6* [Setting things up](#setting-things-up)
7* [Hello world](#hello-world)
8* [Intermezzo I: What is WebAssembly?](#intermezzo-i-what-is-webassembly)
9* [Building a library](#building-a-library)
10* [Intermezzo II: JavaScript and the DOM](#intermezzo-ii-javascript-and-the-dom)
11* [Loading the library and making it a module](#loading-the-library-and-making-it-a-module)
12* [Multithreading](#multithreading)
13* [Intermezzo III: Web Workers and Spectre](#intermezzo-iii-web-workers-and-spectre)
14* [Don't block the main thread!](#dont-block-the-main-thread)
15* [Callback functions](#callback-functions)
16* [Persistent storage](#persistent-storage)
17* [Closing thoughts](#closing-thoughts)
18
19## Introduction
20
21I have recently worked on making a web application out of
22[my latest Rubik's cube optimal solver](https://git.tronto.net/nissy-core/file/README.md.html).
23This involved building a rather complex C code base (with
24multithreading, SIMD, callback functions and whatnot) to
25[WebAssembly](https://en.wikipedia.org/wiki/WebAssembly) via
26[Emscripten](https://emscripten.org/), and writing a minimal amount of
27JavaScript and HTML for the frontend.
28
29This whole process was complex, tiring and at times frustrating -
30but eventually [it was a success](https://tronto.net:48)! Not only
31I accomplished my goal, but I have learnt a lot along the way. After
32finishing the work, I decided to write down all that I have learnt and
33share it with the world with this post.
34
35You may be wondering why one should do such a thing instead of either
36rewriting their code base in a more web-friendly language, or distributing
37their app using a native GUI framework. The main reason to use WebAssembly
38is that it can provide near-native performance (or so they claim) while
39running inside a web browser; this gives you all the portability of a
40web app without too much of a performance drawback, something that would
41not be possible with an interpreted language such as JavaScript.
42
43So, what is this blog post? A tutorial for web development? I am not sure
44about this, but if it is, it is definitely not a normal one. As the title
45suggests, you should not start from this guide unless you just *love*
46banging your head against the wall. If you are looking for a *sane*
47guide to web development, I strongly advise you head on to the
48[Mozilla Developer Network tutorials page](https://developer.mozilla.org/en-US/docs/MDN/Tutorials)
49and start from there.
50
51But if you are a C or C++ developer looking to port a program or library
52to the web, then you are in the right place. With this post I am going
53to walk you through the process of building an increasingly complex
54library that can run in a web browser. Make sure you are
55sitting comfortably and be ready to sweat, because I am not going to
56shy away from the hard stuff and the complicated details.
57
58To follow this tutorial you won't need much experience with web
59development, but some familiarity with HTML and an idea of what JavaScript
60will be useful. It will also help to know that you can access your
61browser's JavaScript console and other developer tools by pressing F12,
62at least on Firefox or Chrome - but I guess I have literally just taught
63you that, if you did not already know it. For all the rest, I'll make
64sure to add many hyperlinks throughout the text, so you can follow them
65if something is new to you.
66
67A little disclaimer: although I am a somewhat experienced C developer,
68I had very little web development experience before embarking in
69this adventure. If you are a web developer, you may find errors in
70this post that are going to make you laugh at my ignorance. If you do,
71I'd appreciate it if you could report them to me by sending an email to
72`sebastiano@tronto.net`!
73
74With this out of the way, let's get started!
75
76## Setting things up
77
78The examples used in this tutorial are all contained in a git repository,
79which you can find either on
80[my git page](https://git.tronto.net/emscripten-tutorial/file/README.md.html) or
81[on github](https://github.com/sebastianotronto/emscripten-tutorial).
82
83In order to follow them you are going to need:
84
85* A working installation of [Emscripten](https://emscripten.org/)
86 (which also includes Node.js). Refer to the official website for
87 installation instructions.
88* A web server such [darkhttpd](https://github.com/emikulic/darkhttpd)
89 or the Python `http.server` package; the examples will use darkhttpd.
90
91I have only tested all of this on Linux, but everything should work
92exactly the same on any UNIX system. If you are a Windows user, you can
93either run everything inside
94[WSL](https://learn.microsoft.com/en-us/windows/wsl/), or you can try and
95adjust the examples to your system - if you choose this second option,
96I'll happily accept patches or pull requests :)
97
98## Hello world
99
100Let's start with the classic Hello World program:
101
102```
103#include <stdio.h>
104
105int main() {
106 printf("Hello, web!\n");
107}
108```
109
110You can compile the code above with
111
112```
113emcc -o index.html hello.c
114```
115
116And if you now start a web server in the current folder, for example with
117`darkhttpd .` (the dot at the end is important), and open a web browser to
118[localhost:8080](http://localhost:8080) (or whatever port your web server
119uses), you should see something like this:
120
121![Hello world in a browser](hello.png)
122
123As you can see, the compiler generated a bunch of extra stuff around
124you print statement. You may or may not want this, but for now we can
125take it as a convenient way to check that our program works as expected.
126
127There are other ways to run this compiled code. With the command above,
128the compiler should have generated for you 3 files:
129
130* `index.html` - the web page in the screenshot above.
131* `index.wasm` - the actual compiled code of your program; this file contains
132 WebAssembly bytecode.
133* `index.js` - some JavaScript *glue code* to make it possible for `index.wasm`
134 to actually run in a browser.
135
136If you don't specify `-o index.html`, or if your specify `-o` followed
137by a filename ending in `.js`, the `.html` page is not going to be
138generated. In this case (but also if you *do* generate the html page),
139you can run the JavaScript code in your terminal with:
140
141```
142node index.js
143```
144
145In later examples, the same code may not work seamlessly in both a web
146browser and in Node.js - for example, when dealing with persistent data
147storage. But until then, we can generate all three files with a single
148command and run our code in either way.
149
150It is also possible to ask Emscripten to generate only the `.wasm` file,
151in case you want to write the JavaScript glue code by yourself. To do
152this, you can pass the `-sSTANDALONE_WASM` option to `emcc`. However,
153in some cases the `.js` file is going to be generated even when this
154option is used, for example when building a source file without a `main()`
155entry point. Since this is something we'll do soon, we can forget about
156this option and just take it as a fact that the `.wasm` files generated
157by emscripten require some glue JavaScript code to actually run,
158but in case you are interested you can check out
159[the official documentation](https://emscripten.org/docs/tools_reference/settings_reference.html#standalone-wasm).
160
161You can find the code for this example, as well as scripts to
162build it and run the web server, in the directory `00_hello_world`
163of the git repository
164([git.tronto.net](https://git.tronto.net/emscripten-tutorial/file/README.md.html),
165[github](https://github.com/sebastianotronto/emscripten-tutorial)).
166
167Anyway, now we can build our C code to run in a web page. But this is
168probably not the way we want to run it. First of all, we don't want to
169use the HTML template provided by Emscripten; but more importantly, we
170probably don't want to write a program that just prints stuff to standard
171output. More likely, we want to write some kind of library of functions
172that can be called from the front-end, so that the user can interact with
173our program via an HTML + JavaScript web page. Before going into that,
174let's take a break to discuss what we are actually compiling our code to.
175
176## Intermezzo I: What is WebAssembly?
177
178![The logo of WebAssembly](wasm.png)
179
180[WebAssembly](https://en.wikipedia.org/wiki/WebAssembly) is a low-level
181language meant to run in a virtual machine inside a web browser. The main
182motivation behind it is running higher-performance web applications compared
183to JavaScript; this is made possible, by its
184compact bytecode and its stack-based virtual machine.
185
186WebAssembly (or WASM for short) is supported by all major browsers
187since around 2017. Interestingly, Emscripten, the compiler we are
188using to translate our C code to WASM, first appeared in 2011,
189predating WASM by a few years. Early on, Emscripten would compile
190C and C++ code into JavaScript, or rather a subset thereof called
191[asm.js](https://en.wikipedia.org/wiki/Asm.js).
192
193Just like regular
194[assembly](https://en.wikipedia.org/wiki/Assembly_language), WASM
195also has a text-based representation. This means that one could write
196WASM code directly, assemble it to bytecode, and then run it. We are
197not going to do it, but if you are curious here is a simple example
198(computing the factorial of a number, taken from Wikipedia):
199
200```
201(func (param i64) (result i64)
202 local.get 0
203 i64.eqz
204 if (result i64)
205 i64.const 1
206 else
207 local.get 0
208 local.get 0
209 i64.const 1
210 i64.sub
211 call 0
212 i64.mul
213 end)
214```
215
216As you can see, it looks like a strange mix of assembly and
217[Lisp](https://en.wikipedia.org/wiki/Lisp_(programming_language)).
218If you want to try and run WASM locally, outside of a web browser,
219you could use something like [Wasmtime](https://wasmtime.dev/).
220
221Until early 2025, the WASM "architecture" was 32-bit only. One big
222limitation that this brings is that you cannot use more that 4GB
223(2<sup>32</sup> bytes) of memory, because pointers are only 32 bits
224long; moreover, your C / C++ code may need some adjustments if it
225relied on the assumption that e.g. `sizeof(size_t) == 8`. At the
226time writing a new standard that enables 64 bit pointers, called
227WASM64, is supported on Firefox and Chrome, but not on Webkit-based
228browsers such as Safari yet. Depending on when you are reading this,
229this may have changed - you can check the status of WASM64 support
230[here](https://webassembly.org/features/).
231
232## Building a library
233
234Back to the main topic. Where were we? Oh yes, we wanted to build
235a C *library* to WASM and call it from JavaScript. Our complex,
236high-performance, math-heavy library probably looks something like this:
237
238library.h (actually, we are not going to need this):
239
240```
241int multiply(int, int);
242```
243
244library.c:
245
246```
247int multiply(int a, int b) {
248 return a * b;
249}
250```
251
252Or maybe it is a bit more complicated than that. But we said we are
253going to build up in complexity, and this is just the beginning, so
254let's stick to `multiply()`.
255
256To build this library you can use:
257
258```
259emcc -o library.js library.c
260```
261
262As we saw before, this is going to generate both a `library.js` and a
263`library.wasm` file. Now we would like to call our library function
264with something like this
265
266program.js:
267
268```
269var library = require("./library.js");
270const result = library.multiply(6, 7);
271console.log("The answer is " + result);
272```
273
274*(The `require()` syntax above is valid when running this code in Node.js,
275but not, for example when running in a browser. We'll see in the next
276session what to do in that case, but for now let's stick to this.)*
277
278Unfortunately, this will not work for a couple of reasons. The reason
279first is that Emscripten is going to add an underscore `_` to all our
280function names; so we'll have to call `library._multiply()`. But this
281still won't work, because by default the compiler does not *export* all
282the functions in your code - that is, it does not make them visible to
283the outside. To specify which functions you want to
284export, you can use the `-sEXPORTED_FUNCTIONS` flag, like so:
285
286```
287emcc -sEXPORTED_FUNCTION=_multiply -o library.js library.c
288```
289
290And now we finally have access to our `multiply()` function...
291
292```
293$ node program.js
294Aborted(Assertion failed: native function `multiply` called before runtime initialization)
295```
296
297...or maybe not. If you are new to JavaScript like I was a few weeks
298ago, you may find this error message surprising. Some runtime must be
299initialized, but can't it just, like... initialize *before* trying to
300run the next instruction?
301
302Things are not that simple. A lot of things in JavaScript happen
303*asynchronously*, and in these situations you'll have to either use
304[`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)
305or a
306[*callback function*](https://developer.mozilla.org/en-US/docs/Glossary/Callback_function).
307So we'll have to do something like this:
308
309```
310var library = require("./build/library.js");
311
312library.onRuntimeInitialized = () => {
313 const result = library._multiply(6, 7);
314 console.log("The answer is " + result);
315};
316```
317
318And now we can finally run our program:
319
320```
321$ node program.js
322The answer is 42
323```
324
325The code for this example can be found in the `01_library` folder in
326the git repository
327([git.tronto.net](https://git.tronto.net/emscripten-tutorial/file/README.md.html),
328[github](https://github.com/sebastianotronto/emscripten-tutorial)).
329
330## Intermezzo II: JavaScript and the DOM
331
332![The logos of HTML, CSS and JavaScript](logos.png)
333
334If we want to build an interactive web page using JavaScript, we'll
335need a way for our script to communicate with the page, i.e. a way
336to access the HTML structure from JavaScript code. What we are looking
337for is called
338*[Document Object Model](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)*,
339or DOM for short.
340
341For example, if you have a paragraph with some text in your HTML:
342
343```
344<p id="myParagraph">Hello!</p>
345```
346
347you can access this text from JavaScript like this:
348
349```
350var paragraph = document.getElementById("myParagraph");
351paragraph.innerText = "New text!";
352```
353
354Here we are selecting the paragraph HTML element using its ID, and we
355are changing its text via its `innerText` property, all from JavaScript.
356
357Let's see a more complex example:
358
359HTML:
360
361```
362<button id="theButton">Press me!</button>
363```
364
365JS:
366
367```
368var button = document.getElementById("theButton");
369var counter = 0;
370
371button.addEventListener("click", () => {
372 counter++;
373 button.innerText = "I have been pressed " + counter + " times!";
374});
375```
376
377In the example above we add an
378*[event listener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)*
379to a button: the (anonymous) function we defined is going to be called
380every time the button is clicked. And since this is a web page, I guess
381I can show you what this actually looks like.
382
383Behold, the dynamic button:
384
385<div style="text-align:center">
386<button id="theButton">Press me!</button>
387</div>
388
389<script>
390window.onload = () => {
391 var button = document.getElementById("theButton");
392 var count = 0;
393
394 button.addEventListener("click", () => {
395 count++;
396 button.innerText = "I have been pressed " + count + " times!"
397 });
398};
399</script>
400
401If you are completely new to web development, you may be wondering
402where you should write this JavaScript code. One option is to write it
403in the same HTML file as the rest of the page, inside a `<script>` tag;
404this is how I did it in the example above, as you can check by viewing
405the source of this page (press Ctrl+U, or right-click and select
406"view source", or prepend `view-source:` to this page's URL; hopefully
407at least one of these methods should work in your browser).
408
409However, if the script gets too large you may want to split it off in
410a separate file, which we'll demonstrate in this next example.
411
412Let's now make a template web page for using our powerful library. Let's
413start with the HTML, which is in large part boilerplate:
414
415index.html:
416
417```
418<!doctype html>
419<html lang="en-US">
420<head>
421 <meta charset="utf-8">
422 <meta name="viewport" content="width=device-width">
423 <title>Multiply two numbers</title>
424 <script src="./script.js" defer></script>
425</head>
426
427<body>
428 <p>
429 <input id="aInput"> x <input id="bInput">
430 <button id="goButton">=</button>
431 <span id="resultText"></span>
432 </p>
433</body>
434
435</html>
436```
437
438Besides the `<body>` element, the only important line for us is line
4397, which loads the script from a file. Notice that we use the `defer`
440keyword here: this is telling the browser to wait until the whole page
441has been loaded before executing the script. If we did not do this, we
442could run in the situation where we `document.getElementById()` returns
443`null`, because the element we are trying to get is not loaded yet (yes,
444this happened to me while I was writing this post). If you want to know
445more, check out this
446[MDN page](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#defer).
447
448Now to the JavaScript code. For now we are going to use the built-in
449`*` operator to multiply the two numbers, but in the next section we
450are going to replace it with our own library.
451
452script.js (in the same folder as index.html):
453
454```
455var aInput = document.getElementById("aInput");
456var bInput = document.getElementById("bInput");
457var button = document.getElementById("goButton");
458var resultText = document.getElementById("resultText");
459
460button.addEventListener("click", () => {
461 var a = Number(aInput.value);
462 var b = Number(bInput.value);
463 resultText.innerText = a * b;
464});
465```
466
467The final result will look something like this:
468
469<p style="text-align:center">
470<input id="aInput"> x <input id="bInput">
471<button id="goButton">=</button>
472<span id="resultText"></span>
473</p>
474
475<script>
476var aInput = document.getElementById("aInput");
477var bInput = document.getElementById("bInput");
478var button = document.getElementById("goButton");
479var resultText = document.getElementById("resultText");
480
481button.addEventListener("click", () => {
482 var a = Number(aInput.value);
483 var b = Number(bInput.value);
484 resultText.innerText = a * b;
485});
486</script>
487
488In a real-world scenario you would probably want to check that the text
489provided in the input fields is actually a number, or perhaps use the
490[`type="number"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/number)
491attribute for the input fields. But we'll ignore these issues here -
492we are going to have more serious problems to deal with.
493
494## Loading the library and making it a module
495
496With what we have learned in the previous intermezzo (you are not skipping
497those, right?) we can finally run our library code in a real web page. The
498code is pretty much the same as above; we just need to include both the
499library and the script file in the HTML:
500
501```
502 <script src="./library.js" defer></script>
503 <script src="./script.js" defer></script>
504```
505
506and of course we have to change the line where we perform the multiplication:
507
508```
509 resultText.innerText = Module._multiply(a, b);
510```
511
512Here `Module` is the default name given to our library by
513Emscripten. Apart from being too generic a name, this leads to another
514problem: we can't include more than one Emscripten-built library in our
515page in this way - otherwise, both are going to be called `Module`.
516
517Luckily, there is another way: we can build a
518[modularized](https://emscripten.org/docs/compiling/Modularized-Output.html)
519library, i.e. obtain a
520[JavaScript Module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules).
521This may sound a bit strange, because the name `Module` kind of implies
522there is already a module. The way I understand it is that by default
523Emscripten produces a *script* that *contains* a module named `Module`;
524when building a modularized library, the whole resulting file is a module.
525
526Modularizing our build is not necessary right now, but
527there are a couple of other advantages to it:
528
529* As mentioned above, we can change the name of our module and include
530 more than one Emscripten-built library, if we want.
531* We will be able to use the module in the same way in Node.js and in
532 our web page script. This way we can minimize the differences between
533 the two versions of our code, which can be useful for testing.
534* In case we want to build a more complex layer of JavaScript between
535 our library and our web page, with a modularized build we can easily
536 include the module in another file, which can then be included in the
537 main script.
538
539So let's go ahead and build our library like so:
540
541```
542emcc -sEXPORTED_FUNCTION=_multiply -sMODULARIZE -sEXPORT_NAME=MyLibrary \
543 -o library.mjs library.c
544```
545
546Notice I have changed the extension from `.js` to `.mjs`. Don't worry,
547either extension can be used. And you are going to run into issues with
548either choice:
549
550* If you run your code in Node.js, it will understand that the library
551 file is a module only if you use the `.mjs` extension. Alternatively,
552 you can change some settings in a local configuration file to
553 enforce this.
554* If you run your code in a web page, your web server may not be
555 configured to serve `.mjs` files as JavaScript files. This can
556 easily be changed by adding a configuration line somewhere.
557
558In my examples I chose to use the `.mjs` extensions to make Node.js
559happy, and I changed the configuration of my web servers as needed. For
560example, for darkhttpd I added a file called `mime.txt` with a single
561line `text/javascript mjs`, and launched the server with the
562`--mimetypes mime.txt` option.
563
564Now we have to make a couple of changes. Our `program.js`, for running
565in node, becomes:
566
567```
568import MyLibrary from "./library.mjs"
569
570var myLibraryInstance = await MyLibrary();
571
572const result = myLibraryInstance(6, 7);
573console.log("The answer is " + result);
574```
575
576By the way, I have renamed this file to `program.mjs`. This is because
577only modules can use the
578[static `import`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)
579statement; alternatively, I could have used the
580[dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)
581and kept the `.js` extension.
582
583Similary, we have to update our `script.js` (or `script.mjs`) to import
584the module and create an instance. Moreover, we have to specify in the
585HTML that the script is now a module:
586
587```
588 <script src="./script.mjs" type="module" defer></script>
589```
590
591And we can get rid of the other `<script>` tag, since now the library
592is included directly in `script.mjs`.
593
594You can find the full the code for this example the folder
595`02_library_modularized` in the git repository
596([git.tronto.net](https://git.tronto.net/emscripten-tutorial/file/README.md.html),
597[github](https://github.com/sebastianotronto/emscripten-tutorial)).
598
599## Multithreading
600
601!["Gotta go fast" meme](threads.jpg)
602
603Let's move on to a more interesting example. If one of the goals of
604WebAssembly is performance, there is no point in using only 1/16th of
605your CPU - let's port a multithreaded application to the web!
606
607As a more complicated example, let's write a function that counts how
608many prime numbers there are in a given range. This function takes two
609integers as input and returns a single integer as output, but it does
610a non-trivial amount of work under the hood. A simple implementation of
611this routine would be something like this:
612
613```
614bool isprime(int n) {
615 if (n < 2)
616 return false;
617
618 for (int i = 2; i*i <= n; i++)
619 if (n % i == 0)
620 return false;
621 return true;
622}
623
624int primes_in_range(int low, int high) {
625 if (low < 0 || high < low)
626 return 0;
627
628 int count = 0;
629 for (int i = low; i < high; i++)
630 if (isprime(i))
631 count++;
632
633 return count;
634}
635```
636
637This algorithm is
638[embarassingly parallelizable](https://en.wikipedia.org/wiki/Embarrassingly_parallel):
639we can split the interval `[low, high)` into smaller sub-intervals and
640process each one of them in a separate thread; then we just need to add
641up the results of the sub-intervals.
642
643For the actual implementation, we are going to use
644[pthreads](https://en.wikipedia.org/wiki/Pthreads), for the simple reason
645that it is
646[supported by Emscripten](https://emscripten.org/docs/porting/pthreads.html).
647In practice, assuming we are working on a UNIX platform, we could also
648use C11's [threads.h](https://en.cppreference.com/w/c/header/threads) or
649C++'s [std::thread](https://en.cppreference.com/w/cpp/thread/thread.html),
650but only because they happen to be wrappers around pthreads. On other
651platforms, or in other implementations of the C and C++ standard library,
652this may not be the case; so we'll stick to old-school pthreads.
653
654This is my parallel version of `primes_in_range()`:
655
656primes.c:
657
658```
659#include <stdbool.h>
660#include <pthread.h>
661
662#define NTHREADS 16
663
664bool isprime(int);
665void *pthread_routine(void *);
666
667struct interval { int low; int high; int count; };
668
669int primes_in_range(int low, int high) {
670 pthread_t threads[NTHREADS];
671 struct interval args[NTHREADS];
672
673 if (low < 0 || high < low)
674 return 0;
675
676 int interval_size = (high-low)/NTHREADS + 1;
677 for (int i = 0; i < NTHREADS; i++) {
678 args[i].low = low + i*interval_size;
679 args[i].high = args[i].low + interval_size;
680 pthread_create(&threads[i], NULL, pthread_routine, &args[i]);
681 }
682
683 int result = 0;
684 for (int i = 0; i < NTHREADS; i++) {
685 pthread_join(threads[i], NULL);
686 result += args[i].count;
687 }
688
689 return result;
690}
691
692bool isprime(int n) {
693 if (n < 2)
694 return false;
695
696 for (int i = 2; i*i <= n; i++)
697 if (n % i == 0)
698 return false;
699 return true;
700}
701
702void *pthread_routine(void *arg) {
703 struct interval *interval = arg;
704
705 interval->count = 0;
706 for (int i = interval->low; i < interval->high; i++)
707 if (isprime(i))
708 interval->count++;
709
710 return NULL;
711}
712```
713
714*(Pro tip: if you take the number of threads as an extra parameter for
715your function, you can pass to it the value
716[`navigator.hardwareConcurrency`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/hardwareConcurrency)
717from the JavaScript front-end and use exactly the maximum number of
718threads that can run in parallel on the host platform.)*
719
720To build this with Emscripten we'll have to pass the `-pthread` option and,
721optionally, a suitable value for
722[`-sPTHREAD_POOL_SIZE`](https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size).
723
724If we want to run our multithreaded code in an actual browser, we'll
725have to scratch our head a bit harder. The code we are supposed to
726write is exactly what we expect, but once again we have to tinker with
727our web server configuration. For technical reasons that we'll cover in
728the next intermezzo, in order to run multithreaded code in a browser
729we must add a couple of HTTP headers:
730
731```
732Cross-Origin-Opener-Policy: same-origin
733Cross-Origin-Embedder-Policy: require-corp
734```
735
736These headers are part of the response your browser will receive when
737it requests any web page from the server. The way you set these depends on
738the server you are using; with darkhttpd you can use the `--header` option.
739
740With your server correctly set up, you can enjoy a multithreaded program
741running in your browser! As always, you can check out this example from
742the `03_threads` folder of the git repository
743([git.tronto.net](https://git.tronto.net/emscripten-tutorial/file/README.md.html),
744[github](https://github.com/sebastianotronto/emscripten-tutorial)).
745
746## Intermezzo III: Web Workers and Spectre
747
748![The logo of the Spectre vulnerability](spectre.png)
749
750On a low level, threads are implemented by Emscripten using
751[web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API),
752which are processes separated from the main web page process and
753communicate with it and with each other by
754[passing messages](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage).
755Web workers are commonly used to run slow operations in the background
756without blocking the UI threads, so the web page remains responsive
757while these operations run - we'll do this in the next section.
758
759Web workers do not have regular access to the same memory as the main
760process, and this is something that will give us some issues in later
761sections. However, there are ways around this limitation. One of these
762ways is provided by
763[SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer),
764which we won't use directly in this tutorial, but is used by
765Emscripten under the hood.
766
767And this is why we had to set the `Cross-Origin-*` headers. In 2018, a
768CPU vulnerability called [Spectre](https://spectreattack.com) was found,
769and it was shown that an attacker could take advantage of shared memory
770between the main browser thread and web workers to
771[execute code remotely](https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)#Remote_exploitation).
772As a counter-measure, most browsers now require your app to be in a
773[secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts)
774and
775[cross-origin isolated](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated)
776to allow using `SharedArrayBuffer`s.
777
778Even if you do not plan to use web workers directly, it is still good to
779have a rough idea of how they work, because of the
780[law of leaky abstractions](https://en.wikipedia.org/wiki/Leaky_abstraction):
781*all abstractions are leaky*.
782The fact that we had to mess around with our `Cross-Origin-*` headers
783despite not caring at all about `SharedArrayBuffer`s is a blatant example
784of this.
785
786## Don't block the main thread!
787
788If you have run the previous example, may have noticed a scary warning
789like this in your browser's console:
790
791![A warning saying "Blocking on the main thread is very dangerous, see [link]"](blocking.png)
792
793*The link points to
794[this page](https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread)
795in Emscripten's documentation.*
796
797The issue here is that our heavy computation is not running "in the
798background", but its main thread (the one spawning the other threads)
799coincides with the browser's main thread, the one that is responsible
800for drawing the UI and handling user interaction. So if our computation
801really takes long, the browser is going to freeze - and after a few
802seconds it will ask us if we want to kill this long-running script.
803
804As we anticipated in the previous intermezzo, we are going to solve this
805with a web worker. We will structure this solution as follows:
806
807* The main script will be responsible for reading the user input, sending
808 a message to the worker to ask it to compute the result, and handling
809 the result that the worker is going to send back once it is done. No
810 slow operation is performed by this script, so that it won't block
811 the main thread.
812* The worker will be responsible for receiving mesages from the main
813 script, handling them by calling the library, and sending a message
814 with the response back once it is done computing.
815
816In practice, this will look like this:
817
818script.mjs:
819
820```
821var aInput = document.getElementById("aInput");
822var bInput = document.getElementById("bInput");
823var button = document.getElementById("goButton");
824var resultText = document.getElementById("resultText");
825
826var worker = new Worker("./worker.mjs", { type: "module" });
827
828button.addEventListener("click", () => worker.postMessage({
829 a: Number(aInput.value),
830 b: Number(bInput.value)
831}));
832
833worker.onmessage = (e) => resultText.innerText = "There are " +
834 e.data.result + " primes between " + e.data.a + " and " + e.data.b;
835```
836
837worker.mjs:
838
839```
840import Primes from "./build/primes.mjs";
841
842var primes = await Primes();
843
844onmessage = (e) => {
845 const count = primes._primes_in_range(e.data.a, e.data.b);
846 postMessage({ result: count, a: e.data.a, b: e.data.b });
847};
848```
849
850More complicated than before, but nothing crazy. Notice how we are using
851[`postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage)
852and
853[`onmessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event)
854to pass events back and forth. The argument of `postMessage()` is the
855actual data we want to send in
856[JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON)
857format, while the argument of `onmessage()` is an
858[event](https://developer.mozilla.org/en-US/docs/Web/API/Event)
859whose `data` property contains the object that was sent with `postMessage()`.
860
861You can check out this example in the directory `04_no_block` in the
862repository
863([git.tronto.net](https://git.tronto.net/emscripten-tutorial/file/README.md.html),
864[github](https://github.com/sebastianotronto/emscripten-tutorial)).
865Try also large numbers, in the range of millions or tens of millions, and
866compare it with the previous example - but not don't go too large, we
867only support 32-bit integers for now. Notice how, with this new setup,
868the browser remains responsive while it is loading the response.
869
870Oh and by the way, a nice exercise for you now could be making the
871script show some kind of `"Loading result..."` message while the worker
872is working. This is not hard to do, but a huge improvement in user
873experience!
874
875## Callback functions
876
877![The hand of a person using a phone](callback.jpg)
878
879For one reason or another, your library function may take as parameter
880another function. For example, you may use this other function to print
881log messages regardless of where your library code is run: a command-line
882tool may pass `printf()` to log to console, while a GUI application
883may want to show these messages to some text area in a window, and it
884will pass the appropriate function pointer parameter. This is the use case
885that we are going to take as an example here, but it is not the only one.
886
887Implementing this was probably the step that took me the longest in my
888endeavor to port my Rubik's cube solver to the web. Luckily for you,
889when writing this post I found a simpler method, so you won't have to
890endure the same pain.
891
892First, we'll have to adapt our library function like this:
893
894```
895int primes_in_range(int low, int high, void (*log)(const char *)) {
896 /* The old code, with calls to log() whenever we want */
897};
898```
899
900*Tip: when using callback functions like this, it is good practice
901to have them accept an extra `void *` parameter, and the library
902function should also accept an extra `void *` parameter that it then
903passes on to the callback. So our function would look something like
904this: `int primes_int_range(int low, int high, void (*log)(const char *, void *), void *log_data)`.
905This makes the setup extremely flexible, and allows passing callback
906functions in situation where this may be tricky. For example, this
907way you could pass a C++ member function by passing an object as
908`log_data` and a function that call `log_data`'s member function
909as `log`. Since we are not going to use this in this example, I'll stick
910to the simpler setup.*
911
912Now, to call our function from the JavaScript side we would like
913to do something like this:
914
915```
916int result = primes_in_range(a, b, console.log); // Logging to console
917```
918
919Unfortunately, this will not work, because `console.log`, a JavaScript
920[function object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function),
921does not get automatically converted to a function *pointer*, which is
922what C expects. So we'll have to do something slightly more complicated:
923
924```
925import Primes from "./build/primes.mjs"
926
927var primes = await Primes();
928const logPtr = primes.addFunction((cstr) => {
929 console.log(primes.UTF8ToString(cstr));
930}, "vp");
931
932const count = primes._primes_in_range(1, 100, logPtr);
933```
934
935Here `addFunction()` is a function generated by Emscripten. Notice also
936that we are wrapping our `console.log()` in a call to `UTF8ToString()`,
937an Emscripten utility to convert C strings to JavaScript strings, and
938that we are passing the function's signature `"vp"` (returns `void`,
939takes a `pointer`) as an argument; see
940[here](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#function-signatures)
941for more information.
942
943Other than that, you just need to add a couple of compiler flags:
944
945* `-sEXPORTED_RUNTIME_METHODS=addFunction,UTF8ToString` to tell the
946 compiler to make these two methods available.
947* `-sALLOW_TABLE_GROWTH` to make it possible to add functions to
948 out module at runtime with `addFunction()`.
949
950And as you can check by running the example `05_callback` from the repo
951([git.tronto.net](https://git.tronto.net/emscripten-tutorial/file/README.md.html),
952[github](https://github.com/sebastianotronto/emscripten-tutorial)),
953everything works as expected, both in Node.js and in a web page. To make
954the examples more interesting, the web page one is not only not logging the
955messages to console, but it also shows them as text in the web page.
956
957*Note: you must be careful where you call this callback function from.
958If you try to call it from outside the main thread - for example, in one
959of the threads that are spawned to count the primes in the sub-intervals
960- you'll get a horrible crash. This is because web workers do not have
961access to the functions that reside in another worker's memory.*
962
963## Persistent storage
964
965![3D rendering of a spinning hard drive](storage.png)
966
967Our multithreaded implementation of `primes_in_range()` is not slow, but
968it could be faster. One possible way to speed it up is to use a look-up
969table to make `is_prime()` run in constant time; for this we'll need to
970memorize which numbers below 2<sup>31</sup> (the maximum value of 32-bit
971signed integer) are prime. This will require 2<sup>31</sup> bits of data,
972or 256MB. It would be nice if we could store this data persistently in
973the user's browser, so that if they use our app again in the future we
974won't need to repeat expensive calculations or re-download large files.
975
976Putting aside the question of whether any of the above is a good idea,
977and assuming you know how to generate such a table, in C you would
978read and store the data like this:
979
980```
981#include <stdio.h>
982
983#define FILENAME "./build/primes_table"
984
985void read_table(unsigned char *table) {
986 FILE *f = fopen(FILENAME, "rb");
987 fread(table, TABLESIZE, 1, f);
988 fclose(f);
989}
990
991void store_table(const unsigned char *table) {
992 FILE *f = fopen(FILENAME, "wb");
993 fwrite(table, TABLESIZE, 1, f);
994 fclose(f);
995}
996```
997
998*Note: the code snippet above is extremely simplified, you probably want
999to add some error-handling code if you implement something like this.*
1000
1001The good news is that we can use the same code when building with
1002Emscripten! The bad news is that... well, it's a bit more complicated
1003than that.
1004
1005First of all, it is important to know that
1006[Emscripten's File System API](https://emscripten.org/docs/api_reference/Filesystem-API.html)
1007supports different "backends", by which I mean ways of translating the
1008C / C++ file operations to WASM / JavaScript. I am not going to discuss
1009all of them here, but I want to highlight a few key points:
1010
1011* The default backend is called `MEMFS`. It is a virtual file system
1012 that resides in RAM, and all data written to it is lost when the
1013 application is closed.
1014* Only one of these backends (`NODERAWFS`) gives access to the actual
1015 local file system, and it is only usable when running your app with
1016 Node.js. Browsers are *sandboxed*, and the filesystem is not normally
1017 accessible to them. There are ways, such as the
1018 [File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API),
1019 to access files, but as far as I understand each file you want to
1020 access requires explicit actions from the user. We would like to manage
1021 our data automatically, so we are not going to use this API.
1022* The backend we are going to use is called `IDBFS`. It provides access
1023 to the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API),
1024 which allows to persistently store large quantities of data in the
1025 browser's cache. The data is only removed if the user asks for it,
1026 for example by cleaning it from the browser's settings page.
1027
1028To activate the `IDBFS` backend, we are going to add `--lidbfs.js`
1029to our compiler options. The Indexed DB is not the only way to store
1030data persistently in the browser. For an overview of all the options,
1031you can take a look at
1032[this page on MDN](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Client-side_APIs/Client-side_storage).
1033
1034The compiler flag is not enough, however. We also need to:
1035
10361. Create a directory (for the virtual file system) where our data file
1037 is going to be stored. We are going to call this directory `assets`,
1038 but you can pick any other name; it does not have to coincide with the
1039 name of a directory that exists on your local file system.
10402. Mount the directory we have just created in the indexed DB.
10413. Synchronize the virtual file system, so that our script is able to
1042 read pre-existing files.
1043
1044All of the above has to be done from JavaScript, which makes things a
1045little bit complicated, because we are reading our files from C code.
1046We have a couple of ways to work around this issue:
1047
1048* Using
1049 [inline JavaScript](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-call-javascript-from-native)
1050 in our C code with the `EM_JS()` or `EM_ASYNC_JS()` Emscripten macros.
1051* Setting up the indexed DB file system when the module loads using
1052 the `--pre-js` compiler option.
1053
1054Here we are going to use the second solution, but the first option is
1055good to keep in mind, because it allows us to call JavaScript code at
1056any point rather than just at startup.
1057
1058*Note: if you do end up using `EM_ASYNC_JS()` to make asynchronous
1059JavasScript functions callable from C, keep in mind that any C
1060function that, directly or indirectly, calls an async JavaScript
1061function, will now return a
1062[promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
1063when called from JavaScript. But wether an async function is called is
1064determined at runtime, so you C function may return a value one time
1065and a promise another time, depending on how exactly it runs!*
1066
1067So we are going to add `--pre-js init_idbfs.js` to our compiler options,
1068with `init_idbfs.js` containing the following:
1069
1070```
1071Module['preRun'] = [
1072 async () => {
1073 const dir = "/assets";
1074
1075 FS.mkdir(dir);
1076 FS.mount(IDBFS, { autoPersist: true }, dir);
1077
1078 Module.fileSystemLoaded = new Promise((resolve, reject) => {
1079 FS.syncfs(true, (err) => {
1080 if (err) reject(err);
1081 else resolve(true);
1082 });
1083 });
1084
1085 }
1086];
1087```
1088
1089As you can see, the syncing operation is more complicated, the main
1090reason being that it is an
1091[asynchronous operation](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Async_JS).
1092For this reason, we are wrapping it in a
1093[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise),
1094so we can detect when this operation is done and react accordingly.
1095We are going to do so from our worker script, which will send a message to
1096the main script to communicate that the file system is ready to go:
1097
1098```
1099primes.fileSystemLoaded.then(() => {
1100 postMessage({ type: "readySignal" });
1101});
1102```
1103
1104The main script can then handle this signal as it prefers, for example by
1105enabling the `Compute` button, if it was previously marked as `disabled`.
1106
1107One last thing: since we are now using a large amount of memory and
1108loading the virtual file system at the start, the compiler will complain
1109that we are not reserving enough memory for our application. Adding a
1110`-sINITIAL_MEMORY=272629760` compiler flag will do the trick (watch out:
1111the number you provide must be a multiple of 2<sup>16</sup>). I am not
1112entirely sure why this is the case, since we are not loading the file in
1113memory statically, but only at runtime, and only when the
1114`primes_in_range()` function is called. I would expect that using
1115[`-sALLOW_MEMORY_GROWTH`](https://emscripten.org/docs/tools_reference/settings_reference.html#allow-memory-growth)
1116would be enough - and indeed this is the case if we use the `EM_ASYNC_JS()`
1117macro to load the file system on-demand.
1118
1119And with all this, we are ready to run our optimized version of the
1120`primes_in_range()` algorithm, all from within our browser! As always,
1121you can check out the complete code in the folder `06_storage` of
1122the repository
1123([git.tronto.net](https://git.tronto.net/emscripten-tutorial/file/README.md.html),
1124[github](https://github.com/sebastianotronto/emscripten-tutorial)).
1125
1126If generating this data on the user's side seems redundant, you can
1127also have it downloaded from the server. I won't explain how to it here,
1128since there are many possible ways to achieve this - after all, the indexed
1129DB is also accessible from JavaScript. If you want to experiment more
1130with Emscripten you can try to use the
1131[Fetch API](https://emscripten.org/docs/api_reference/fetch.html); in my
1132project I was not able to make its synchronous version work together with
1133`-sMODULARIZE`, so I ended up using
1134[`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
1135directly from within an `EM_ASYNC_JS()` function. This tutorial is already
1136too long, so I am going to leave this as an exercise for the reader.
1137
1138## Closing thoughts
1139
1140I have discussed almost everything that I have learned about building a
1141webapp in C / C++ with Emscripten. I ended up using C, not C++, for all
1142of my example, so I did not have a chance to discuss some neat C++-specific
1143features such as
1144[`EMBIND()`](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html)
1145and
1146[`emscripten::val`](https://emscripten.org/docs/api_reference/val.h.html)
1147- do check them out if you plan to use C++ for your web app!
1148
1149Even if this page is structured like a tutorial, this is probably better
1150described as a collection of personal notes, a "brain dump" that I wrote
1151for myself as is the case with many of my blog posts. Writing this piece
1152was a great occasion for me to review the work that I have done and the
1153things I have learned. And while reflecting on all of this I was able to
1154isolate a specific impression that I had while working on this,
1155and I summarized it in on sentence:
1156
1157<center><strong><i>
1158It's leaky abstractions all the way down.
1159</i></strong></center>
1160
1161If you have not encountered this term before (but you should, I have already
1162used it in this post), *leaky abstraction* is a term used to describe the
1163failure of an abstraction to hide the low-level details it is abstracting.
1164The so-called
1165[law of leaky abstractions](https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/)
1166says that all abtractions are leaky. But, in my opinion not all
1167abstractions leak in the same way - some leak way more than others.
1168
1169Emscripten is a great project that tries to abstract away all the web
1170(JavaScript, WASM, web workers, local storage...) so that you can build
1171and run your C / C++ code in a web browser. Frankly, this is mind-blowing,
1172and I have mad respect for the Emscripten developers.
1173
1174But as soon as the complexity of your codebase bumps up a notch, you
1175immediately find out that the abstractions don't hold anymore. If yor
1176app is multithreaded, you have to learn what a web worker is. If you
1177want to read some data from a file, welcome to the world of client-side
1178storage. You need 64-bit memory support because you are processing more
1179than 2GB of data? Sure, but first make sure that your users are not
1180using Safari.
1181
1182But I am not complaining about this. A browser is a very different beast
1183from a bare-metal operating system, and it is to be expected that you
1184have to know something about the system you are deploying to. I am happy
1185that I could learn about all of this, and I believe this knowledge is
1186going to give me an extra edge whenever I'll work on the web again.

Generated with cgit - Back to sebastiano.tronto.net