Home

Awesome

A highly-experimental, likely-pretty-slow polyfill for WebAssembly

I want to learn about the binary encoding and execution semantics of WebAssembly, and trying to write a polyfill seemed like as good a way to do that as any. I make no promises about this ever being useful for Real Work. But it's fun!

In this repository we have:

Current Status

It works, but it's pretty slow.

When run under node, the polyfill passes the full spec interpreter test suite. When run under spidermonkey it passes all but some float-related tests, and I suspect that's because the tests need to be updated to account for different handling of NaNs [1].

The code can also load and run the WASM AngryBots demo. It currently runs so slowly as to be nigh unplayable, but there's still some low-hanging fruit to improve performance of the generated JavaScript; see below for some ideas..

[1] https://github.com/WebAssembly/spec/issues/286

How to Use

First, consider whether this highly-experimental code is right for you. If you're just looking to compile some code to the web and have it run, you'll almost certainly be better served by the more mature WebAssembly support in the emscripten toolchain:

https://github.com/kripken/emscripten/wiki/WebAssembly

But if you're trying to do something unusual, like JIT to WASM at runtime in the browser, them a full-blown polyfill may be necessary.

Next, make sure you've built it into a standalone JS file::

> make

Then you can load it into a webpage like this:

<script type="text/javascript" src="./wasm-polyfill.js"></script>
<script type="text/javascript">
// This uses the browser's builtin WebAssembly if present,
// and the polyfill if not.
var funcs = WebAssembly.instantiate("wasm code here")
</script>

Or load it as a module in node:

var WebAssembly = require('wasm-polyfill.js')
var funcs = WebAssembly.instantiate("wasm code here")

Or if you're feel really adventurous, you can load ./webextension/manifest.json as a WebExtension in Firefox or Chrome and have it available on all webpages.

I've used this trick to successfully load and run the AngryBots demo, albeit slowly.

Theory of Operation

The polyfill works by parsing the WebAssembly binary format and translating each function into semantically-equivalent javascript. The translation process prioritizes the following, in order:

  1. The generated code must be semantically correct, even for various edge-cases where the semantics of WASM do not map cleanly onto the semantics of JavaScript.

  2. The generated code should run fast. We try to generate as close to valid asmjs as possible, and will spend CPU cycles during translation if they result in significantly improved code (such as eliminating lots of bounds checks).

  3. The code generation should be done quickly, since we're running on the client. This means we won't try to do a lot of micro-optimization of the generated code.

So far it does a good job of (1), but there's a lot of work remaining on (2), and I haven't looked into (3) much at all. But hey, it's early days for this code! :-)

As a concrete example, given this simple WebAssembly implementation of a 32-bit factorial function:

(module
  (func (export "fac-rec") (param i64) (result i64)
    (if i64 (i64.eq (get_local 0) (i64.const 0))
      (i64.const 1)
      (i64.mul (get_local 0) (call 0 (i64.sub (get_local 0) (i64.const 1))))
    )
  )
)

The polyfill will produce a JavaScript function that looks like:

function (WebAssembly, asmlib, imports) {
  
  // Create and annotate the exported functions.
  var funcs = asmfuncs(asmlib, imports)

  funcs.F0._wasmTypeSigStr = 'i_i'
  funcs.F0._wasmJSWrapper = null

  var exports = {}
  exports['fac-rec'] = funcs.F0

  // An inner asmjs-style function containing all the code.
  // In this case we're able to generate valid asmjs, but
  // that's not always the case in general.

  function asmfuncs(stdlib, foreign, heap) {
    "use asm"

    var i32_mul = foreign.i32_mul 

    function F0(li0) {
      li0 = li0|0
      var ti0 = 0
      var ti1 = 0
      var ti2 = 0
      if ((((li0)|0)==((0)|0))|0) { L1: do {
        ti0 = (1)|0
      } while(0)} else { L1: do {
        ti1 = (li0)|0
        ti2 = (F0((((li0)|0)-((1)|0))|0))|0
        ti0 = (i32_mul((ti1)|0,(ti2)|0))|0
      } while(0) }
      return ti0
    }

    return {
      F0: F0
    }
  }

  // If there were initializers to run, they'd go here.

  return exports
}

For this simple function, we're able to generate something that's valid asmjs and should run pretty fast. That's not always the case in general. Some of the tricky parts, where WASM differs from asmjs and makes a direct translation difficult, include: