With the advent of WebAssembly, more and more C/C++ source code is being compiled and run in browsers. Browsers don’t expose the user’s file system, however Emscripten has created a virtual file system that stores all the files in memory (or persists them to IndexedDB). This is great for porting programs to the web or even creating workflows that reload when you return to the same page, however often times the users want to actually download the files they are working with. Enter this handy snippet:

function download(filenamePtr, dataPtr, size) {
const a = document.createElement('a')
a.style = 'display:none'
document.body.appendChild(a)
const view = new Uint8Array(Module.HEAPU8.buffer, dataPtr, size)
const blob = new Blob([view], {
type: 'octet/stream'
})
const url = window.URL.createObjectURL(blob)
a.href = url
const filename = UTF8ToString(filenamePtr)
a.download = filename
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
}
window.download = download
view raw download.js hosted with ❤ by GitHub

This snippet creates a hidden anchor/link and sets it to a special url that points to a binary blob, and then auto-magically clicks it to start the download. In WebAssembly (or asmjs) memory is allocated as a single contiguous ArrayBuffer which Emscripten exposes as Module.HEAPU8 and friends. Pointers in WebAssembly are actually just integer offsets into this array. Therefore, we can pass a pointer to this download function and a size, and use the Uint8Array to wrap that range.

Passing strings is also not straightforward (unless you use the newer Embind library). When we pass a char* from C++ into JavaScript, remember that we’re really just passing an integer offset into memory. You could attempt to read the memory from Module.HEAPU8, however Emscripten provides a helper function UTF8ToString that does this for us.

Now all we need to do is call it from C++. Emscripten includes a few primitives such as EM_ASM_ to do this:

#include <emscripten.h>
#include <string.h>

int main(void) {
    const char* filename = "hello.txt";
    const char* text = "Hello world!";
    EM_ASM_({ window.download($0, $1, $2) }, filename, text, strlen(text));
    return 0;
}

Emscripten supports a command line option --pre-js download.js that automatically includes the above file into our output JavaScript.

emcc --pre-js download.js main.cpp -o main.html

After running it, I get a popup and it indeed downloaded the file.

Happy coding!