Emscripten Notes - C from Desktop to Web
Over the past while I've been using Emscripten to make web builds of my games. This page is a collection of notes about using it on Linux, and some of the changes needed to adapt code to run in the browser. All the lessons were learned by working on Faur, my personal C framework.
Setup#
Install from Git Repo#
Throughout this page, $EMSCRIPTEN
refers to the Emscripten SDK's absolute installation path. I like to put all 3rd party SDKs in /opt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Update#
1 2 3 4 |
|
When I was first checking out Emscripten, it targeted asm.js and implemented a partial SDL 1.2 API. Today it builds Web Assembly binaries and the full SDL 2 from source!
You can check the available SDK versions with
./emsdk list
.
Reset & Start Over#
Sometimes odd things break and you want to reset and re-install everything:
1 2 3 4 5 |
|
The git clean
command removes all files that are not part of the emsdk repo:
-d
removes directories not just files-f
is to allow removing without an explicit permission set in .git/config-x
is to remove files even if they match .gitignore rules, like build output might.
Building and Running#
Building with Regular GNU Make#
Emscripten comes with its own build tools like emcc, emmake, and more that correspond to their standard equivalents (see them in $EMSCRIPTEN/upstream/emscripten). To build your program with a makefile called Makefile.emscripten
, you would open Bash and run:
1 2 3 4 5 |
|
However, I want to build with just make -f Makefile.emscripten
like for all other platforms, without any other extra stuff. I solved this with a recursive makefile that I run with normal GNU Make, and which then calls emmake on itself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
This conveniently sets the emsdk environment variables as part of the same call, too. Note that Bash is one of the shells that is known to be friendly with emsdk_env.sh
.
See my Gamebuino Makefile notes for another example of this pattern in action.
Emscripten Settings#
Check out $EMSCRIPTEN/upstream/emscripten/src/settings.js
for all the available flags. I build my programs with these, which I pass to the compilers and linker:
1 2 3 4 5 6 7 8 9 10 11 |
|
These flags tell Emscripten what libraries to build with, to target Web Assembly, and to allow the available run-time memory to grow as needed.
Apparently,
ALLOW_MEMORY_GROWTH
has low performance with asm.js, but is not an issue with WASM. You can also specify the available RAM at program start withINITIAL_MEMORY
.
Show Application on an HTML Page#
If your build target ends with .html
, Emscripten will automatically generate an HTML shell for your application. When you build without optimizations, with -O0
, you can see all the un-minified HTML and JavaScript that loads your program, and learn how to customize it.
You can link with --shell-file path/to/shell.html
to specify your own template. Here is my own default shell.html, based on the one generated by Emscripten. Note the special {{{ SCRIPT }}}
tag near the end, which is replaced at build-time with JavaScript code that loads the WASM program.
Run Application on Local Computer#
Browsers impose some security-related restrictions on pages loaded from the local file system instead of from a server. This means that you can't just open the generated HTML file to run the program, the code will not load.
I work around this with a Python 3 script that spins a server from the target directory and points Firefox to the appropriate address. The script runs as part of the Emscripten makefile's make run
target, so I get the same dev flow as if I was working on a desktop app. C is great, but Python is my build-time secret weapon:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
C Language Details#
Compiler Flags#
I build C and C++ with -pedantic -pedantic-errors -Werror
across as many platforms as possible, with some exceptions for old toolchains with very outdated compilers. For Emscripten, I build clean with those settings and with -std=gnu11
for C and -std=gnu++11
for C++. I usually stick to ISO standards, but Emscripten requires GNU for EM_ASM blocks.
If you build with -pedantic, then you may also need -Wno-dollar-in-identifier-extension
, because EM_ASM block arguments are assigned automatic names like $0
, $1
, etc. which are not allowed by the language standard. Similarly, -Wno-gnu-zero-variadic-macro-arguments
allows EM_ASM blocks without any arguments.
EM_ASM blocks are for writing inline JavaScript in C code, like you sometimes see assembly language being used on other platforms:
1 2 3 4 5 6 |
|
Casting Function Pointers#
Whether you target asm.js or WASM, function pointers are small integers like 1
, 2
, or 0x38
instead of larger values like 0x56242647907e
. These numbers are indexes in function tables instead of addresses to function code, and the pointer type associated with the value identifies the table. When you cast a function pointer to a different type and call it, you end up reading an entry from the wrong table.
Thankfully, void and typed data pointers resolve to the same signature, which allows convenient patterns like using functions with typed pointer arguments in places that take callbacks with generic void* parameters. Being able to write functions like void my_cleanup(mytype* object)
instead of void my_cleanup(void* object)
is a small win for succinct and readable C.
Note that while convenient, this pattern is undefined behavior according to the C standard. The function types are incompatible because
mytype*
andvoid*
are incompatible types. Technically, you're not supposed to cast and call one as if it was the other, although doing so is alright as long as your compiler implements the expected behavior.
While functions with matching void* and mytype* arguments might be interchangeable in practice, functions with and functions without return values are definitely not. I once changed a function that used to not return anything to return a number, but I overlooked a place where it was still being used as a callback without a return value... The program kept running fine on amd64 and ARM, but it threw an exception in the browser because it read at an invalid index from the wrong function table.
Order of Evaluating Function Arguments#
When you make a call like f(g(x), h(y))
, does it matter which one of g(x)
and h(y)
runs first? If it does, or if you want to guarantee the same order on every platform, then you need to evaluate them separately beforehand:
1 2 3 4 |
|
I hit this problem while working on procedurally-generated game levels. Both g
and h
used the same random number generator, and they each received different values from it depending on the order they ran. The generated levels looked different between the desktop and web game builds, despite starting from the same PRNG seed.
Program Flow#
The Infinite Game Loop#
The old-school game loop might look something like this:
1 2 3 |
|
Unfortunately this simple pattern does not work here. An infinite game loop would never give the browser a chance to handle user input or render any updates. Instead, the pattern is to run a single game frame and hand control back to the browser. You do so by registering a frame callback and giving up control of execution:
1 2 3 4 5 6 7 8 9 10 |
|
In the call to emscripten_set_main_loop
,
0
lets the browser decide how often to call your callback, like v-sync. You can pass a positive integer to set a specific frame rate.true
stops execution at this point, your next code that runs will be the loop callback.
Exiting the Program#
Emscripten programs call emscripten_force_exit(int code)
where they would normally call exit(int code)
. I use this function at the end of a fatal error handler routine, otherwise the application naturally exits when the browser is closed.
Using Files#
Embedded Files#
Emscripten makes it easy to bundle up and embed files and directories for the application to use, like music and images. You can add them with --preload-file
linker flags that identify local files and set their run-time paths.
Here is an example project with separate trees for assets, source code, and build files:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
You would build Project.html like this:
1 2 3 |
|
If the application wants to access Project/assets/image.png
as assets/image.png
at run-time, and given that we are building from Project/build/
, the preload flag should look like so:
1 2 |
|
Finally, you can declare all the files and automate the flags:
1 2 |
|
Writing Files to Persistent Storage#
Emscripten uses the ephemeral MEMFS as the default backing file system, so everything your program writes is lost when the browser is closed. To persist files between sessions, you need to use IDBFS instead.
IDBFS has to be initialized before you can use it, and you have to link with -lidbfs.js
. The init code should be placed somewhere early in main
, before any other code that uses files. In this example, /my-idbfs
is the starting point to all R/W paths, so the files you might work with would look like /my-idbfs/hiscore.sav
, not ./hiscore.sav
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
The Module.fs_is_ready
flag is set in the post-init callback, which runs asynchronously. Module
is a global Emscripten object that represents the running application. It is created in the HTML shell, and is a convenient place to attach our own JavaScript context data, like this flag.
IDBFS starts up very quickly, so to keep things simple the loop callback could just drop every frame until the post-init callback sets fs_is_ready
. Going back to the loop example from before:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Finally, I like to queue a file system storage sync after every write. Note FS.syncfs
's boolean direction parameter: we want to flush file system memory to storage after a write, while previously we wanted to initialize the memory with data from storage.
1 2 |
|
Using the SDL 2 Library#
Getting the Browser Window Size#
This is useful for making a full-size screen that fills the entire window:
1 2 |
|
You can then pass these values to SDL_CreateWindow, SDL_RenderSetLogicalSize, etc. as appropriate. I use this together with a special HTML shell that shows a full-sized canvas screen and nothing else.
Enable Sound in Chrome#
Annoying autoplay videos are why we can't have nice things. Chrome, and maybe other browsers too, automatically block audio on a page so your new application will be muted.
There are ways to enable audio programmatically, but they have to happen in the call stack of a user-initiated interaction, like a key press or a mouse click. This JavaScript code in the HTML shell fixes SDL 2 sound:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
audio_fix
is a handler for key and click events, so the fix will run in the context of a user interaction as required. You can find more extensive fixes that go beyond just SDL 2 in the GitHub issue linked above, but this was all I needed myself.
Using Game Controllers in Web Browsers#
In the interest of user privacy, browsers do not automatically allow web pages to use game controllers. First the page has to load, then the user needs to press an input on the controller, and finally your program must handle an add-controller event prompted by the user's press.
So, the common pattern of opening all the attached controllers in a loop at start-up does not work with Emscripten. SDL_NumJoysticks will always show 0 then:
1 2 3 |
|
Typical SDL applications have an event loop that runs every frame, where you can now also handle the Added and Removed controller events:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Epilogue#
Emscripten and Arduino are some of my framework's most unusual build targets. I think cross-platform software benefits most from these sort of systems, because they have interesting platform-specific needs that you have to abstract away, in turn making the entire project more robust and more portable.