Giorgio's Blog

More Emscripten: the shared file system in a web application

Previously, I covered how to build native code (a game-like sample written in C++) for the web. That post covered Emscripten, a powerful tool for transforming native code into WebAssembly. That sample contained asset files such as the images, music and sounds a game typically includes.

What if you wanted to read or write non-asset files in your application? Suppose a game that has a high-score table for instance. With Emscripten you can do this too! Not just from the native side, but also from the frontend's JavaScript code.

The project: a breakout clone with kittens

a breakout clone running on a mac

For today's post, I've written a small game using Raylib that runs on most computers. The game in question is a clone of the Atari classic, Breakout. I won't go into detail about the game here, but it was a fun game to build for the purposes of this demo.

Our goal is to take this code, and leverage Emscripten's file system from the frontend's side to dynamically alter the running game. Specifically, we'll retrieve an image from placekitten.com, write it to /tmp, and then load it into the game.

To build the project, we can use a similar build process to the previous post to get this into the browser. Here is an excerpt of linker flags we pass to the compiler:

-lraylib \
-lm \
-s USE_GLFW=3 \
-s GL_ENABLE_GET_PROC_ADDRESS \
-s WASM=1 \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-s ENVIRONMENT=web \
-s EXPORTED_RUNTIME_METHODS=['FS'] \
--embed-file ./assets \
-O3

Like last time, we link against the GLFW, Raylib and standard math libraries. Then, we specify a few flags that handle the generated JavaScript module. There's two flags here that are particularly important:

Reading files from native code

Just like last time, we specified an --embed-file flag containing a directory of assets to ship inside of our wasm file. Once specified, this takes care of the work needed for us to leverage standard io functions on our asset files. What about other files though? It turns out, Emscripten does the lifting for us to have a POSIX-like environment to read and write files from.

To demonstrate this, here's a peek at our loadCat method:

void Game::loadCat() {
    UnloadTexture(this->bg);
    this->bg = LoadTexture("/tmp/cat.png");
}

This method removes the stock background and replaces it with a PNG file located within /tmp. What is /tmp though? Per the API docs:

File data in Emscripten is partitioned by mounted file systems. Several file systems are provided. An instance of MEMFS is mounted to / by default. The subdirectories /home/web_user and /tmp are also created automatically, in addition to several other special devices and streams (e.g. /dev/null, /dev/random, /dev/stdin, /proc/self/fd); see FS.staticInit() in the FS library for full details.

Essentially, the FS API provides enough of an abstraction that our native code can read/write to an in-memory file system that looks like a typical POSIX one. This is particularly handy, since our C++ game doesn't need to worry about where it's running.

Indeed, if you were to build and run this game on a Linux or MacOS system, you could place a PNG file in /tmp and call this method successfully!

Writing files from JavaScript

How do we get the file into /tmp in a browser? The generated wasm module has an FS object that we can use for this purpose. Here's a peek at our kitten() function from the JavaScript side:

async function kitten() {
    if (!gameModule) {
        return;
    }

    // download the cat as a binary blob
    const catto = await (await fetch('https://placekitten.com/g/640/480')).blob();

    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    
    // convert the blob to an img-src
    const cattoImg = await blobToImage(catto);

    // set up our temporary canvas for drawing into it
    canvas.height = cattoImg.naturalHeight;
    canvas.width = cattoImg.naturalWidth;
    context.drawImage(cattoImg, 0, 0, cattoImg.naturalWidth, cattoImg.naturalHeight);
    const canvasBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png', 1));
    const bufferView = new Int8Array(await canvasBlob.arrayBuffer());
    
    gameModule.FS.writeFile(
        '/tmp/cat.png',
        bufferView
    );
    
    // inform native code it is time to load the cat!
    gameModule['_loadCat']();
}

There's a few things going on here. Let's break it down:

With our file writing and file reading handlers in place, all that's left to do is provide a way for the user to invoke all this code. That means wiring up a button on the page, and presto:

a breakout clone running in the browser with dynamic file loading elements

Final thoughts

A breakout clone is something we could have written entirely in JavaScript. Why go through all of this effort? For simple games such as this one, it might have made sense to do just that. As with many things in computers, there are tradeoffs to consider. More complex applications might have performance or porting considerations that make choosing JavaScript impractical.

Furthermore, the benefits of the web environment to enable creating new experiences cannot be discounted. We took a game and extended it for the web in a unique way. There are other ways to extend this sample. Consider a drag-and-drop area that allows the user to change the blocks or paddle. The sky's the limit!

  1. Raylib does support loading JPEGs, but it is good form to normalize the content we load. Consider if our image service returned a format that was incompatible with our graphics library 💥

  2. We chose to do this on the JavaScript side for the purposes of demonstrating the power of the Emscripten file system API. We could have chosen to do this from our native code using a library like ImageMagick, though.