- A replacement for JavaScript
- The one-stop easy shop for super-charging your existing web applications
- A total replacement for native apps
WASM is really fast, right?
Yes! Oh dear sweet goodness, yes. Thereās a pretty good video editor demo here showing off a handful of video algorithms. For the rest of the article, Iāll be talking about my own 3D character animation experiment (code with link to live demo here), which was written carefully to compare JavaScript and WASM without any major algorithmic differences. There are fundamental differences to how C-style arrays and structs work compared to JavaScript arrays and objects, but thatās somewhat unavoidableāI could have hacked in C-style things to JavaScript typed arrays, but I wanted to keep the code idiomatic where possible. See this document for general architecture.
These are the results I observed after running 15 high-detail animated characters with 4 possible animations on desktop platforms and 8 animated characters on mobile platforms. These benchmarks are somewhat rough (especially the Android one), but they do make their point well:
For reference, 60 frames per second (FPS) is the maximum allowable by most displays and translates to an average frame time of 16.6 MS. Running 60 FPS provides a crisp and smooth experience. Movies and TV generally run at 24 FPS, which requires an average frame time of around 42 MS. Anything less than 10 FPS is unacceptably choppy for anything animated, the bar for which is 100 MS per frame.
Developing with WASM
For the rest of the article, Iām going to talk about C++ exclusively for development, though WASM is not strictly C++. The tools for WASM are very new, with C++ and Rust being the only supported languages Iāve seen used so far.Build Tools
Despite being very new, the tools are quite smooth. I used Emscripten, and after a little bit of a difficulty initially setting up the compiler (in these early days, you have to check out the āincomingā branch), I was able to compile to WASM using a command very similar to standard GCC compile commands. For simpler tests, I was able to use the tools Emscripten uses under the hood as well: Clang, LLVM, and Binaryen. In these early days of WebAssembly, I did have to bring in the incoming Emscripten branch, like so:kamaron@kamaron:~/emsdk-portable$ ./emsdk install sdk-incoming-64bit
kamaron@kamaron:~/emsdk-portable$ ./emsdk activate sdk-incoming-64bit
After that, building was pretty easy. Set up environment variables to make life easy...
kamaron@kamaron:~/Desktop/wasm-demo$ pushd ~/emsdk-portable
kamaron@kamaron:~emsdk-portable$ source ./emsdk_env.sh
kamaron@kamaron:~emsdk-portable$ popd
⦠and then just run `emcc` in much the same way you would run `gcc`:
kamaron@kamaron:~/Desktop/wasm-demo$ emcc ./cc/naivewasmanimationmanager.cc -O3 -s SIDE_MODULE=1 -s WASM=1 -o ./webroot/naive.wasm
This generates a WASM file that can be consumed by the browser, which leads me to...
JavaScript API and communication to/from C++
A WASM file is used to generate a WebAssembly module (think of it as the executable), which can then be instantiated as a WebAssembly instance (think of that as the process). This instance is sandboxed and accessible from your JavaScript code. Creating a WASM module is very simple, especially with ES6 Promisesāfetch the binary code from the network, run it through a pretty easy command, and bam! Module. Easy as pie and just three lines of code. Instantiating the module is where you provide the WASM memory object and JavaScript-exported functions that your C++ code needs. As an interesting note, this may include a lot of inner functionality used in the C++ standard libraries. Math functions like sin and cos are often implemented using raw assembly segments, which cannot be used directly in WASM. Here is what I ended up using for my demo (comments added for context): naivewasmanimationmanager.tsfetch('naive.wasm')
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.compile(bytes))
.then((wasmModule: WASMModule) => {
this.memory = new WebAssembly.Memory({ initial: 512 });
const imports = {
env: {
memoryBase: 0,
tableBase: 0,
memory: this.memory,
table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
// STL required function imports
_acosf: Math.acos,
_sinf: Math.sin,
_fmodf: (a: number, b: number) => { return a % b; },
// Self-used utility function imports
_alertError: (code: number, line: number, extra: number) => {
console.error(errCodes.get(code) || '', ' ON LINE ', line, ' EXTRA:: ', extra);
}
}
};
return WebAssembly.instantiate(wasmModule, imports);
}
)
// ⦠code ā¦
public getSingleAnimation(animation: Animation, model: ModelData, animationTime: number): AnimationResult {
// Some less-relevant code...
var rsl = new Float32Array(this.memory.buffer, this.nextMemoryOpen, MAT4_SIZE * model.boneNames.length / FLOAT_SIZE);
this.exports._getSingleAnimation(this.nextMemoryOpen, this.animationAddresses.get(animation), this.modelAddresses.get(model), animationTime);
return rsl;
}
public getBlendedAnimation(animations: [Animation, Animation], model: ModelData, animationTimes: [number, number], blendFactor: number): AnimationResult {
if (!this.memory) return new AnimationResult(new Float32Array([]), 0);
this.exports._getBlendedAnimation(
this.nextMemoryOpen,
this.animationAddresses.get(animations[0]),
this.animationAddresses.get(animations[1]),
this.modelAddresses.get(model),
animationTimes[0],
animationTimes[1],
blendFactor
);
return new AnimationResult(new Float32Array(this.memory.buffer, this.nextMemoryOpen, MAT4_SIZE * model.boneNames.length / FLOAT_SIZE), 0);
}
naivewasmanimationmanager.cc
extern "C"
{
extern void alertError(uint32_t, uint32_t, uint32_t);
}
// ⦠code⦠code⦠codeā¦
extern "C"
{
void getSingleAnimation(Mat4* rslBuffer, Animation* animation, ModelData* model, float animationTime)
{
// ⦠implementation, which outputs by writing to rslBuffer
}
void getBlendedAnimation(Mat4* rslBuffer, Animation* a1, Animation* a2, ModelData* model, float t1, float t2, float blendFactor)
{
// ⦠implementation, which outputs by writing to rslBuffer
}
}
Once the module is instantiated, all functions exported from the C++ code are available in the āexportsā object attached to the instance. Generally, these are all functions in your C++ code, so I recommend using extern āCā for the ones youāre interested inā preventing name mangling performed by the C++ compiler.
So whatās the catch?
Just like any tool, there are times when WASM is and isnāt appropriate to use. Here are some things youāll need to know before deciding to use it in your own project:WebAssembly can only communicate with numbers
WebAssembly supports integers and floats, coming in 32- and 64-bit varieties. Thatās it. The JavaScript number can be coerced into any of those, which is nice. C++ structs boil down nicely using only numeric types (all data are bytes eventually), but JavaScript objects and arrays donāt translate so easily. Youāll have to put in a fair amount of logic to translate JavaScript objects to/from their C++ equivalents, as well as a scheme to lay it out manually in the heap yourself. More on that in a bit. The big implication here is that translating objects from WASM to JS and vice versa will incur a cost in performance and code complexity. Try to keep the communication as narrow as possible, unless the communication only involves numbers. Write distinct modules in WebAssembly, not just individual classes or function implementations.JavaScript is responsible for all memory management
WebAssembly uses a memory heap that must be provided from the JavaScript. From the JavaScript, this is essentially a wrapper around an ArrayBuffer. From C++ code, this looks just like the normal heapādereference a pointer, and you get a variable containing the memory as it exists at that location in the heap. Fun fact: this means you could write valid code that dereferences NULL, with 0x00 being a valid address to the beginning of said JS ArrayBuffer. Please donāt do that. Every time you do, a fairy dies. Having access to the WASM instance memory via JS ArrayBuffer is nice, because it lets you access the memory from both the JS and WASM sides of your application. Unfortunately, you donāt have access to system allocation commands like āmallocā and āfree,ā so you essentially have to write your own. Stanford students rejoiceā that CS107 lab will come in handy. Using tools like Emscripten can also definitely help you ease this pain, as a lot of that is boilerplate that can be reused.Debugging
Dear reader, by the time you read this, I hope the next paragraph is obsolete. I have good reason to believe it will be. But if you, like me, are stuck in the before-times, patiently awaiting the implementation of WASM source maps, the next paragraph contains bad tidings indeed. Currently, there are no source maps, but that is something that is in the sights of the WebAssembly group. The text and binary formats for WebAssembly are interchangeable, so browsers can show you the text format for your code. However, that functionality is less helpful than it sounds because WAST (WebAssembly Text format) appears to be some unholy combination of assembly and LISP. If something goes wrong in your WASM code, it is extremely difficult to find. Protip: The C++ __LINE__ macro will be your best friend, as that gives you a helpful number to pass to the JavaScript in the case of a problem. Thankfully, I picked a project that has generally hilarious bugs, so the feeling of wanting to gnaw my arm off was sprinkled with humor at the ridiculous things the dancing bots did:
