WebAssembly bare bones WASI browser polyfill

Use a basic, bare bones browser WASI polyfill to get going quickly with Emscripten-less WebAssembly modules.

Pre-requisites

To demonstrate the use of the bare bones WASI browser polyfill you'll need ready made WebAssembly module. Something that prints "Hello World!" to stdout will be just great.

You will also need to be comfortable with using the command line as some of the steps require you to run commands.

Naturally, you will also need a browser capable of running WebAssembly to test out our WebAssembly module. Any post-2017 browser should work.

Why a Emscripten-less bare bones WASI browser polyfill?

Because all the other WASI polyfills for browsers that I came across (Emscripten, Wasmer-JS) are overly complex and non-trivial to wire up. Great guidance is to follow the KISS principle. Keep it simple stupid! This polyfill is straight JavaScript and allows anyone starting out with the most basic building blocks of WebAssembly quickly.

Preparing to use the bare bones WASI browser polyfill

Before we do anything, let's first create a webassembly_barebones_wasi directory in our local system for our project. Start a terminal (or command prompt) and run the following commands:

mkdir webassembly_barebones_wasi
cd webassembly_barebones_wasi

Place your ready-made WebAssembly module (under the name of hellworld.wasm) in this directory.

For your convenience, I have prepared all the source files (including a basic "hello world" WebAssembly module) we'll be using in this guide in this zip file. You can download this file and extract its contents or create the files manually by following the next steps.

A HTML page to demo the bare bones WASI browser polyfill

We'll need a HTML page for the JavaScript WASI polyfill code and to run our WebAssembly module. So create abarebones_wasi.html source file in the webassembly_barebones_wasi directory with the following contents:


<html>
<head>
    <h1>Bare bones WASI browser polyfill demo</h1>
</head>

    <body><script>

        var barebonesWASI = function() {

                var moduleInstanceExports = null;

                var WASI_ESUCCESS = 0;
                var WASI_EBADF = 8;
                var WASI_EINVAL = 28;
                var WASI_ENOSYS = 52;

                var WASI_STDOUT_FILENO = 1;

                function setModuleInstance(instance) {

                    moduleInstanceExports = instance.exports;
                }

                function getModuleMemoryDataView() {
                    // call this any time you'll be reading or writing to a module's memory 
                    // the returned DataView tends to be dissaociated with the module's memory buffer at the will of the WebAssembly engine 
                    // cache the returned DataView at your own peril!!

                    return new DataView(moduleInstanceExports.memory.buffer);
                }

                function fd_prestat_get(fd, bufPtr) {

                    return WASI_EBADF;
                }

                function fd_prestat_dir_name(fd, pathPtr, pathLen) {

                     return WASI_EINVAL;
                }

                function environ_sizes_get(environCount, environBufSize) {

                    var view = getModuleMemoryDataView();

                    view.setUint32(environCount, 0, !0);
                    view.setUint32(environBufSize, 0, !0);

                    return WASI_ESUCCESS;
                }

                function environ_get(environ, environBuf) {

                    return WASI_ESUCCESS;
                }

                function args_sizes_get(argc, argvBufSize) {

                    var view = getModuleMemoryDataView();

                    view.setUint32(argc, 0, !0);
                    view.setUint32(argvBufSize, 0, !0);

                    return WASI_ESUCCESS;
                }

                 function args_get(argv, argvBuf) {

                    return WASI_ESUCCESS;
                }

                function fd_fdstat_get(fd, bufPtr) {

                    var view = getModuleMemoryDataView();

                    view.setUint8(bufPtr, fd);
                    view.setUint16(bufPtr + 2, 0, !0);
                    view.setUint16(bufPtr + 4, 0, !0);

                    function setBigUint64(byteOffset, value, littleEndian) {

                        var lowWord = value;
                        var highWord = 0;

                        view.setUint32(littleEndian ? 0 : 4, lowWord, littleEndian);
                        view.setUint32(littleEndian ? 4 : 0, highWord, littleEndian);
                   }

                    setBigUint64(bufPtr + 8, 0, !0);
                    setBigUint64(bufPtr + 8 + 8, 0, !0);

                    return WASI_ESUCCESS;
                }

                function fd_write(fd, iovs, iovsLen, nwritten) {

                    var view = getModuleMemoryDataView();

                    var written = 0;
                    var bufferBytes = [];                   

                    function getiovs(iovs, iovsLen) {
                        // iovs* -> [iov, iov, ...]
                        // __wasi_ciovec_t {
                        //   void* buf,
                        //   size_t buf_len,
                        // }
                        var buffers = Array.from({ length: iovsLen }, function (_, i) {
                               var ptr = iovs + i * 8;
                               var buf = view.getUint32(ptr, !0);
                               var bufLen = view.getUint32(ptr + 4, !0);

                               return new Uint8Array(moduleInstanceExports.memory.buffer, buf, bufLen);
                            });

                        return buffers;
                    }

                    var buffers = getiovs(iovs, iovsLen);
                    function writev(iov) {

                        for (var b = 0; b < iov.byteLength; b++) {

                           bufferBytes.push(iov[b]);
                        }

                        written += b;
                    }

                    buffers.forEach(writev);

                    if (fd === WASI_STDOUT_FILENO) console.log(String.fromCharCode.apply(null, bufferBytes));                            

                    view.setUint32(nwritten, written, !0);

                    return WASI_ESUCCESS;
                }

                function poll_oneoff(sin, sout, nsubscriptions, nevents) {

                    return WASI_ENOSYS;
                }

                function proc_exit(rval) {

                    return WASI_ENOSYS;
                }

                function fd_close(fd) {

                    return WASI_ENOSYS;
                }

                function fd_seek(fd, offset, whence, newOffsetPtr) {

                }

                function fd_close(fd) {

                    return WASI_ENOSYS;
                }

                return {
                    setModuleInstance : setModuleInstance,
                    environ_sizes_get : environ_sizes_get,
                    args_sizes_get : args_sizes_get,
                    fd_prestat_get : fd_prestat_get,
                    fd_fdstat_get : fd_fdstat_get,
                    fd_write : fd_write,
                    fd_prestat_dir_name : fd_prestat_dir_name,
                    environ_get : environ_get,
                    args_get : args_get,
                    poll_oneoff : poll_oneoff,
                    proc_exit : proc_exit,
                    fd_close : fd_close,
                    fd_seek : fd_seek
                }               
            }

            function importWasmModule(moduleName, wasiPolyfill) {

                var memory = new WebAssembly.Memory({ initial: 2, maximum: 10 });
                const moduleImports = { wasi_unstable: wasiPolyfill, env : {}, js: { mem: memory } };

                (async () => {
                    var module = null;

                    if (WebAssembly.compileStreaming) {
                      module = await WebAssembly.compileStreaming(fetch(moduleName));
                    }
                    else {
                      const response = await fetch(moduleName);
                      const buffer = await response.arrayBuffer();
                      module = await WebAssembly.compile(buffer);
                    }

                    const instance = await WebAssembly.instantiate(module, moduleImports);

                    wasiPolyfill.setModuleInstance(instance);
                    instance.exports._start();
                })();
            }

            var wasiPolyfill = new barebonesWASI();
            importWasmModule("helloworld.wasm", wasiPolyfill);
    </script>
    Open the browser console to see the output from the WebAssembly module.
</body></html>

Yes! We're now ready to test out the barebones WASI browser polyfill

Because browsers will only accept WebAssembly files if they are served with a mime-type of application/wasm we're unable to use Python's basic built-in SimpleHTTPServer module. So, let's wrap Python's Simple HTTP Server and provide the appropiate mimetype for WebAssembly files into a wasm_server.py file (in the directory):

import BaseHTTPServer, SimpleHTTPServer

SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map['.wasm'] = 'application/wasm'

httpd = BaseHTTPServer.HTTPServer(('localhost', 8000), SimpleHTTPServer.SimpleHTTPRequestHandler)
httpd.serve_forever()

Let's test it out. In your favourite shell, let's start our WebAssembly aware web server:

python wasm_server.py

Point your WebAssembly aware browser to http://localhost:8000/barebones_wasi.html and open your browser console to see the output from the helloworld WebAssembly module!

If all went well, you should see "Hello World from WebAssembly" already in the browser console. Congratulations, you have run your WebAssembly module with a Emscripten-less WASI browser polyfill.

Where to next?

Using the bare bones WASI polyfill, you were able to get a Emscripten-less WebAssembly module successfully executing. This bare bones WASI polyfill implementation is a great starting point that you can use to build more advanced WebAssembly projects.

What do you think?
Have you done anything cool with WebAssembly?
Get in touch via twitter.

Read more articles and tutorials about WebAssembly.

Contact

If you have any questions feel free to contact me directly via: