Cross-compiling C for the web
February 16, 2023  |  c wasm |  GitHubIn this post we’ll take a look at how to use Clang and Emscripten to make C code that can cross-compile to WebAssembly and run in a web page.
First, we will take a look at what WebAssembly is and how to compile simple functions to target the web with Clang. Second, we will create a cross-platform “Hello, world!” app with Emscripten. Then we’ll get more general file i/o working on the web. Finally, we will get a Raylib app set up to cross-compile to the browser.
In order to follow along you will need the Clang compiler and lld. Second, you will need to have Emscripten installed. You will also need a web server to test your web builds; I will use Go and provide a simple server, but use whatever you are comfortable with. I’ll also use make for build scrips and git to pull source code.
All shell commands will be written for a Unix-like system, I was on x86_64 Linux.
What is WebAssembly?
When targeting web platforms, old methods would often translate native source code to JavaScript. In the past several years, however, a new option has become available: WebAssembly. WebAssembly is a portable assembly language used to create binary executables that run on a simple stack-based virtual machine. It strives to provide performance far closer to native binaries when compared with JavaScript.
Let us take our favorite fibonacci function written in C and
compile it to WASM so that it can be called from JavaScript
and used in a web app. We’ll start by making a new project
directory and creating a
fibonacci.c
file.
$ echo "
int fibonacci(int n) {
if(n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
" > fibonacci.c
The clang
compiler can target wasm32
(32 bit WebAssembly)
right out of the box.
$ mkdir static
$ clang fibonacci.c -o static/fibonacci.wasm --target=wasm32 \
--no-standard-libraries \
-Wl,--export-all -Wl,--no-entry
The --target=wasm32
flag tells the compiler to output 32 bit
WebAssembly. Next, --no-standard-libraries
tells the compiler to
omit libc as clang does not come with a WASM compatible
implementation of libc; the libc on your local machine contains
a lot of code that won’t naturally translate to WASM such as
operating system calls. Finally, -Wl,--export-all
tells the linker
to export all
functions and -Wl,--no-entry
tells the linker that we are not
making a runable binary.
Now that we have a fibonacci.wasm
file containing our fibonacci
function, we need to write an HTML file and a JavaScript snippet to
load and run our WebAssembly.
$ echo "
<!DOCTYPE html>
<html>
<script>
WebAssembly.instantiateStreaming(fetch("fibonacci.wasm"), {}).then(
(obj) => console.log(
"The 6th Fibonacci number is " +
obj.instance.exports.fibonacci(6)
)
);
</script>
</html>
" > static/index.html
To test our web app we just need a web server to host it. I will be using the following Go web server.
$ echo "
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc(
"/",
func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "static/"+r.URL.Path[1:])
})
log.Fatal(http.ListenAndServe(":8081", nil))
}
" > server.go
Run the webserver to host the static/
directory.
$ go run server.go
Visiting localhost:8081 in a web browser will produce… a blank screen. But, if we open the developer console, we should see the expected output!
The 6th Fibonacci number is 8
Hello Emscripten
In the last section we saw how simple functions can be compiled to WASM, but what if we want to make an entire application cross-platform? Well, at minimum we need an implementation of the C standard library that works in the browser (or I suppose you could implement a cross-platform standard library of your own as a replacement to libc).
Luckily, there is an available solution: Emscripten! Emscripten
provides the emcc
compiler and linker which emulates POSIX operating
system features in the browser, including a web compatible libc and OpenGL.
Let’s make the classic C intro program.
$ echo "
#include <stdio.h>
int main() {
printf("Hello, emcc!\n");
return 0;
}
" > main.c
Compiling and running this on our local system we should get the expected print out.
$ clang main.c -o hello
$ ./hello
Hello, emcc!
In order to get this working in Emscripten we need a little bit of setup. First, let us make a static directory for our webserver to host.
$ mkdir static
Now we can compile our code with emcc
. The emcc
compiler has three
different output targets:
- WebAssembly: creates a raw
.wasm
file. - JavaScript: adds a
.js
file containing a Module object that binds native functionality to web features: e.g. stdout is mapped to `console.log. - HTML: uses a template to generate a
.html
file to load and run the application. The default HTML template provides several features for testing, such as a canvas for graphical windows and an in page console.
The best way to learn about how emcc
works, in my opinion, is to
simply read through the generated Javascript bindings.
In this post I will outline how to create our own HTML file to
make use of the emcc generated WASM and JavaScript files.
$ emcc main.c -o static/hello.js
Let’s add a bare bones static/index.html
file to load our compiled web
app.
echo "
<!DOCTYPE html>
<html>
<script src="hello.js"></script>
</html>
" > static/index/html
Looking in the static directory, we should now have our index.html
file, as well as two emcc
generated files: hello.js
and
hello.wasm
.
To test our web app we can host it using the same server.go
file
described earlier.
$ go run server.go
Visit localhost:8081, open the developer console, and we should see the greating!
Configuring the Emscripten Module
What if we wanted to change where stdout goes? Well, all we need to
do is take a look at hello.js
. The comment at the top of the file
explains that the Emscripten bindings are controlled via a Module
object. The Module
object can either be created and customized
externally
before the hello.js
script is loaded, or a default Module will be
created. At the bottom of the file a function run
is defined and
called (if Module['noInitialRun']
is not true
).
Currently we are using the defualt Module
. Looking through
hello.js
a
bit more we can find the spot where stdout and stderr
behaviour is defined.
$ cat -n static/hello.js | grep "var err =\|var out ="
276 var out = Module['print'] || console.log.bind(console);
277 var err = Module['printErr'] || console.error.bind(console);
If we want to set our own output function, we can simply define the
Module
object and set Module['print']
to whatever we want.
echo "
<!DOCTYPE html>
<html>
<script>
var Module = {
print: window.alert.bind(window),
};
</script>
<script src="hello.js"></script>
</html>
" > static/index.html
Now if we run the web server and open the page we get an annoying
alert box with our message. It is probably best to switch
back to console.log
if you countinue using this project
as a base for the following examples.
File I/O
When running a native application we have a local file system to use for reading and writing data. Emscripten provides a way to emulate this functionality.
Starting from the basic “hello emcc” project created above,
let us modify main.c
to open a file and print some of its contents.
$ echo "
#include <stdio.h>
int main() {
char buffer[32] = { 0 };
FILE* f = fopen("static/hello.txt", "r");
fread(buffer, sizeof(char), 31, f);
printf("File contained: %s\n", buffer);
return 0;
}
" > main.c
Next we’ll create a static/hello.txt
file to be read by our app.
$ mkdir files
$ echo "I am a file!" > files/hello.txt
Compiling and running on our local system, we get the expected output.
$ clang main.c -o hello
$ ./hello
File contained: I am a file!
However, if we compile and test our file on the web, the console shows an error.
Uncaught (in promise) RuntimeError: index out of bounds
createExportWrapper http://localhost:8081/hello.js:903
callMain http://localhost:8081/hello.js:4318
doRun http://localhost:8081/hello.js:4371
run http://localhost:8081/hello.js:4386
runCaller http://localhost:8081/hello.js:4303
removeRunDependency http://localhost:8081/hello.js:832
receiveInstance http://localhost:8081/hello.js:991
receiveInstantiationResult http://localhost:8081/hello.js:1009
We never told emcc
about our files/hello.txt
file, so of course the
WebAssembly app cannot find it. In order to package a file or
directory of files alongside our code, we need to use
a compiler flag to tell emcc
to include them.
$ emcc main.c -o static/hello.js --preload-file files/
Looking in the static directory we now see that a third generated
file has appeard: hello.data
. Reloading our web app we should now
see the same console output that our local binary produced.
A resizable Raylib app
Porting command line applications to the web is not terribly useful, so let’s do something a bit more complicated. The goal of this section is to create a resizable window drawing some centered text. On our local system this will be a standard window in whatever window manager is being used, e.g. X. On the web our “window” will be a canvas that fills the content area of the browser window and resizes with it.
Setup
Start once again with the hello emcc example from the first section. For this project we will need to download the Raylib source code.
$ git clone https://github.com/raysan5/raylib.git
To compile Raylib for your local system you will need to follow the instructions found on the GitHub readme. The easiest way to get Raylib working is to use a package manager to install the library and its dependencies. Local compilation examples below will assume that the Raylib libraries are already installed.
Thankfully, compiling
Raylib for the web does not require
any dependencies other than the Raylib source and the emcc
compiler.
The Raylib library is written with Emscripten support
and the Makefile
has an option to target web platforms. Lets
use make
to build an Emscripten compatible library file.
$ cd raylib/src/
$ PLATFORM=PLATFORM_WEB make
$ cd ../../
$ mkdir libweb
$ cp raylib/src/libraylib.a libweb/
It will also be nice to grab the Raylib header file and put it in a dedicated include directory for our project.
$ mkdir include
$ cp raylib/src/raylib.h include/
Drawing a square window
Next let us modify main.c to use Raylib to create a 300 pixel square window and fill it with a dark gray background.
echo "
#include <raylib.h>
int main() {
InitWindow(300, 300, "hello, raylib!");
while(!WindowShouldClose()) {
BeginDrawing();
ClearBackground(DARKGRAY);
EndDrawing();
}
return 0;
}
" > main.c
Our build commands in this section will get fairly involved, so we
will head things off by making a Makefile
(feel free to replace
this with your build script of choice).
echo "
LOAD = -lraylib
INCLUDE = -Iinclude
CC = clang
LIB =
FLAGS =
EMCC = EMCC
WLIB = -Llibweb
WFLAGS = -s USE_GLFW=3 -s ASYNCIFY
all: native web
native: main.c
$(CC) main.c -o hello $(FLAGS) $(LIB) $(INCLUDE) $(LOAD)
web: main.c
$(EMCC) main.c -o static/hello.js $(WFLAGS) $(WLIB) $(INCLUDE) $(LOAD)
clean:
rm hello static/hello.js static/hello.wasm
" > Makefile
Running make
with the above Makefile
will produce the following
build commands.
$ clang main.c -o hello -Iinclude -lraylib
$ emcc main.c -o static/hello.js -Iinclude -Llibweb \
-lraylib -s USE_GLFW=3 -s ASYNCIFY
The clang
build command assumes that Raylib is already
in the main build path, i.e. installed via a package manager. You
could modify the variables in Makefile
to match your setup.
The -s USE_GLFW=3
flag tells the compiler we will be using
gflw version 3. The -s ASYNCIFY
tells the compiler
to modify our code to allow it to interact with asynchronous
JavaScript. Currently our code is an infinite loop, which would block
out all JavaScript events if taken literally. Both flags are
necessary in our case because the Raylib library file requires them.
You should now be able to run make
to build for both native and
web. Testing on web we should now get an error.
Uncaught (in promise) TypeError: Module.canvas is undefined
Emscripten requires an HTML canvas to draw a graphical window. If we
look through staic/hello.js
we can find the relevant property is
Module['canvas']
. Let us modify our HTML file to add a canvas
element.
$ echo "
<!DOCTYPE html>
<html>
<body>
<canvas id="canvas"></canvas>
</body>
<script>
var Module = {
canvas: document.getElementById('canvas'),
};
</script>
<script src="hello.js"></script>
<html>
" > static/index.html
Refreshing the page, we now see a 300 pixel gray square.
Using the Emscripten animation loop
Currently our code runs a simple infinite loop no matter the platform. On the web it is best practice to run an “asynchronous loop,” that is, to define a function that contains the body of the loop and then have the browser regularly call that function to request animation frames.
$ echo "
#include <raylib.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
void update() {
BeginDrawing();
ClearBackground(DARKGRAY);
EndDrawing();
}
int main() {
InitWindow(300, 300, "hello, raylib!");
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(update, 0, 1);
#else
while(!WindowShouldClose()) {
update();
}
#endif
return 0;
}
" > main.c
The #ifdef __EMSCRIPTEN__
preprocessor condition allows us to check
whether the emcc
compiler is being used. If the compiler is emcc
,
we set update
as the Emscripten main loop callback function.
Otherwise, we simply run the animation loop as normal.
Making the window resizable
In order to make a window resizable in a native Raylib application,
we simply need to set a config flag before calling
InitWindow
.
SetConfigFlags(FLAG_WINDOW_RESIZABLE);
InitWindow(300, 300, "hello, raylib!");
Let us also make the update
function draw some centered text so we
can ensure that window is re-drawing correctly.
void update() {
char* text = "Hello, emcc!";
BeginDrawing();
ClearBackground(DARKGRAY);
int width = GetScreenWidth();
int height = GetScreenHeight();
int twidth = MeasureText(text, 22);
int theight = 10;
DrawText(
text,
(width - twidth) / 2,
(height - theight) / 2,
22,
RAYWHITE
);
EndDrawing();
}
Making and re-running both the native and web builds, we should see that the native window is now resizable. Unfortunately, the web app still doesn’t resize.
The first issue is that the “window” in the context of the web application is the canvas, not the browser window. However, simply making the canvas fill the browser window does not fix the problem since Emscripten does not emulate the canvas resizing as a window resize.
The solution is to do things manually by taking advantage of another feature of Emscripten: cwrap. We will first need to create a C function that should be called whenever the canvas is resized.
#ifdef __EMSCRIPTEN__
extern void on_resize(int width, int height) {
SetWindowSize(width, height);
}
#endif
Then we will add the following compile flags to our Makefile
to
tell emcc
to export our new on_resize
function in addition to
main
.
WFLAGS = -s USE_GLFW=3 -s ASYNCIFY \
-s EXPORTED_RUNTIME_METHODS=cwrap \
-s EXPORTED_FUNCTIONS=_main,_on_resize
We also tell emcc
to generate cwrap
functionality which allows us
to wrap exported C functions as JavaScript functions.
If we call make
with our updated Makefile
and look through
static/hello.js
we can find the defintion of cwrap
.
static/raysize.js | grep "Module\['cwrap'\]"
$ cat Module['cwrap'] = cwrap;
$ cat static/raysize.js | grep -A 2 "cwrap ="
var cwrap = (ident, returnType, argTypes, opts) => {
return (...args) => ccall(ident, returnType, argTypes, args, opts);
; }
Not delving too much into the inner workings,
Module.cwrap
requires three
arguments: ident
is the name of the exported C function to
be wrapped, returnType
is the return type, and argTypes
is an
array indicating the function arguments. In our case ident
should
be "on_resize
“, returnType
should be null
, and argTypes
should be [number, number]
.
To put our newly exported function into use with cwrap
we need to
make some major modifications to static/index.html
.
$ echo "
<!DOCTYPE html>
<html>
<body>
<canvas id="canvas"></canvas>
</body>
<script>
function on_load() {
let canvas = document.getElementById(
"canvas"
);
let on_resize = Module.cwrap(
"on_resize",
null,
["number", "number"]
);
let resize_handler = () => {
const w = canvas.width
= window.innerWidth;
const h = canvas.height
= window.innerHeight;
on_resize(w, h);
}
window.addEventListener(
"resize",
resize_handler,
true
);
resize_handler();
}
var Module = {
postRun: [on_load],
canvas: document.getElementById('canvas'),
};
</script>
<script src="hello.js"></script>
</html>
" > static/index.html
The above changes make it so that
the canvas resizes with the browser window and the on_resize
function is called when the canvas is
resized. The postRun
property of Module
is an array of functions
to be called after the Emscripten module is running.
We need our on_load
function to be called after Module
has been
configured because we require the Module.cwrap
function.
Recompiling, everything should work as expected and the window should resize on web!
If we want to have the canvas completely fill the
browser window and eliminate any white edges, we can add a header
with some CSS styling to static/index.html
.
<head>
<title>Hello, emcc!</title>
<style>
* {
padding: 0;
margin: 0;
}
body {
overflow: hidden;
}
</style>
</head>
Finally, here is a complete look at the three files we wrote in this section.
$ cat main.c
#include <raylib.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#ifdef __EMSCRIPTEN__
extern void on_resize(int width, int height) {
SetWindowSize(width, height);
}
#endif
void update() {
char* text = "Hello, emcc!";
BeginDrawing();
ClearBackground(DARKGRAY);
int width = GetScreenWidth();
int height = GetScreenHeight();
int twidth = MeasureText(text, 22);
int theight = 10;
DrawText(
text,
(width - twidth) / 2,
(height - theight) / 2,
22,
RAYWHITE
);
EndDrawing();
}
int main() {
SetConfigFlags(FLAG_WINDOW_RESIZABLE);
InitWindow(300, 300, "hello, raylib!");
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(update, 0, 1);
#else
while(!WindowShouldClose()) {
update();
}
#endif
return 0;
}
cat static/index.html
<!DOCTYPE html>
<html>
<head>
<title>Hello Emscripten</title>
<style>
* {
padding: 0;
margin: 0;
}
body {
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script>
function on_load() {
let canvas = document.getElementById(
"canvas"
);
let on_resize = Module.cwrap(
"on_resize",
null,
["number", "number"]
);
let resize_handler = () => {
const w = canvas.width
= window.innerWidth;
const h = canvas.height
= window.innerHeight;
on_resize(w, h);
}
window.addEventListener(
"resize",
resize_handler,
true
);
resize_handler();
}
var Module = {
postRun: [on_load],
canvas: document.getElementById('canvas'),
};
</script>
<script src="hello.js"></script>
</html>
cat Makefile
LOAD = -lraylib
INCLUDE = -Iinclude
CC = clang
LIB =
FLAGS =
EMCC = emcc
WLIB = -Llibweb
WFLAGS = -s USE_GLFW=3 -s ASYNCIFY \
-s EXPORTED_RUNTIME_METHODS=cwrap \
-s EXPORTED_FUNCTIONS=_main,_on_resize
all: native web
native: main.c
$(CC) main.c -o hello $(FLAGS) $(LIB) $(INCLUDE) $(LOAD)
web: main.c
$(EMCC) main.c -o static/hello.js $(WFLAGS) $(WLIB) $(INCLUDE) $(LOAD)
clean:
rm hello static/hello.js static/hello.wasm