Improved WebAssembly Support is Coming to Node.js

WebAssembly is a collection of open standards that have the potential to revolutionize web development. According to webassembly.org:

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications.

Essentially, WebAssembly allows code written in other languages to be compiled down to a binary format that can be loaded and run from JavaScript (or any other environment that embeds a WebAssembly virtual machine). Some of WebAssembly's other notable properties include:

  • Code written in other languages can be more easily repurposed without being completely rewritten in JavaScript.
  • WebAssembly offers near native performance that is more predictable than JavaScript. JavaScript's high level, dynamic nature, along with its use of a garbage collector makes predictable performance a bit of a challenge. Being a compilation target, and not a language of its own, WebAssembly is able to avoid many of JavaScript's performance pitfalls.
  • WebAssembly is designed to be portable. Providing a common compilation target means that platform differences are less of an issue. For example, WebAssembly uses 8-bit bytes, two's complement integers, and is little-endian. Code compiled to WebAssembly does not need to concern itself with the architectural details of the underlying machine.
  • Like JavaScript, WebAssembly is sandboxed. Wasm code does not have unfettered access to the surrounding system. Instead, Wasm communicates with its host via user-defined imports and exports. Although no technology can claim to be 100% secure, Wasm does attempt to mitigate some common attacks via techniques such as bounds checking memory accesses and protecting the call stack from being overwritten.

WebAssembly Text Format

As previously mentioned, WebAssembly is a binary format intended to be a compilation target. This means that developers do not write WebAssembly directly. In most scenarios, code written in a language like C or Rust is compiled into a .wasm file containing the WebAssembly binary code. However, a WebAssembly text format, known as wat, also exists. Binary WebAssembly code can be translated to wat for debugging purposes. wat can also be written directly in order to create very simple WebAssembly modules.

Before exploring wat in more detail, please setup the WebAssembly Binary Toolkit. This toolkit, referred to as WABT, is a collection of utilities for working with WebAssembly modules. One of these utilities, wat2wasm, is used to translate .wat text to the binary .wasm format. There is also an online version of wat2wasm that does not require installing anything locally.

Once wat2wasm is setup, create a file named example.wat containing the following code. This wat code defines a module that exports a single function, addTwo(). This function takes two 32-bit integers as inputs, and returns a 32-bit integer as its result. As the function name implies, the two inputs are added together, and the result returned.

(module  (func (export "addTwo") (param $num1 i32) (param $num2 i32) (result i32)    get_local $num1    get_local $num2    i32.add))

The i32.add operation is responsible for performing 32-bit integer addition. However, it's not immediately clear how i32.add knows what numbers to add, or how the function knows to return the result of the addition. Recall that the earlier quote from webassembly.org describes WebAssembly as an instruction format for a stack-based virtual machine. The term stack-based means that WebAssembly operates by pushing to and popping from an execution stack. In our simple example, the two get_local instructions cause the inputs $num1 and $num2 to be read and pushed onto the stack. The i32.add instruction pops two 32-bit integers off of the stack, adds them together, and pushes the result back on to the stack. Finally, because addTwo() expects to return a 32-bit integer, it pops the sum off of the stack and returns the result.

The remainder of this blog post will build on this example. Create an example.wasm file using the following command.

$ wat2wasm example.wat -o example.wasm

The WebAssembly JavaScript API

Once a Wasm file has been created, it can be imported in JavaScript code via a collection of standard APIs. Current versions of the major browsers support these APIs by default. WebAssembly was enabled by default in V8 5.7, and Node 8 became the first LTS release line to expose WebAssembly without any command line flags.

To use a WebAssembly module from JavaScript code, the Wasm file must be loaded, compiled, and instantiated. To load the Wasm file in Node.js, use fs.readFile() or a similar API. The compilation step creates a stateless WebAssembly.Module from the raw Wasm bytes. Compilation is performed via the WebAssembly.compile() method.

Once a WebAssembly.Module has been created, it can be instantiated any number of times, with each instantiation resulting in a new WebAssembly.Instance. Instantiation is handled by the WebAssembly.instantiate() method. It's worth noting that WebAssembly.instantiate() is capable of performing both the compilation step and the first instantiation, rendering WebAssembly.compile() unnecessary in many cases.

The following example shows how our example Wasm module can be used from JavaScript code both with and without an explicit compilation step. When executed, the following example should print the number 13, the sum of 8 and 5, two times.

'use strict';const fs = require('fs');const bytes = fs.readFileSync('./example.wasm');(async () => {  // Explicitly compile and then instantiate the wasm module.  const module = await WebAssembly.compile(bytes);  const instance = await WebAssembly.instantiate(module);  console.log(instance.exports.addTwo(8, 5));})();(async () => {  // Instantiate the Wasm module with an implicit compilation step.  const { instance, module } = await WebAssembly.instantiate(bytes);  console.log(instance.exports.addTwo(8, 5));})();

Importing WebAssembly Modules

The recently released Node 12.3.0 added --experimental-wasm-modules, a new command line option that allows .wasm files to be imported like JavaScript and JSON files. For example, assume a file named example.mjs containing the following ECMAScript module code.

import * as example from './example.wasm';console.log(example.addTwo(8, 5));

The previous code can be executed in Node 12.3.0 using the following command. You should see the number 13 output.

$ node --experimental-modules --experimental-wasm-modules example.mjs

It's worth pointing out that the previous example uses ECMAScript module syntax. However, at the time of writing, the large majority of the Node.js ecosystem still uses require() and the legacy module system. Fortunately, these users can experiment with these new features via dynamic import(), as shown in the following example. Note that Node must still be launched with the --experimental-modules and --experimental-wasm-modules command line options.

'use strict';(async () => {  const example = await import('./example.wasm');  console.log(example.addTwo(8, 5));})();

WebAssembly System Interface

In March of this year, Mozilla announced the WebAssembly System Interface, or WASI. As its name implies, the goal of WASI is to provide a standard interface between WebAssembly and the underlying platform. This interface defines a collection of system calls that WebAssembly code can invoke. Platforms that support WASI provide the implementation of these system calls. This allows the same WebAssembly code to execute on any platform that supports WASI.

Although WASI is still very young, it opens up significant opportunities for WebAssembly outside of the browser. Recently, a pull request adding experimental WASI module support was opened on the Node.js project. The majority of the WASI implementation can be found in the file lib/internal/wasi.js. While this feature is not available in Node.js yet, the pull request is still worth the read if you're interested in learning more about WASI. Note that the tests written in C code must be compiled with clang using the wasi-sysroot. Instructions for setting up these tools are available via this excellent blog post.

Conclusion

This blog post has provided a high level overview of WebAssembly as a technology, and introduced some of the latest Wasm related features coming to Node.js. However, because WebAssembly encompasses so much, it was not feasible to cover the entire technology in this short blog post. For example, this post did not cover the streaming JavaScript APIs, compiling from languages like C and Rust to Wasm, and much more. If you're interested in learning more about WebAssembly, feel free to reach out at any time.

Don't forget, Joyent offers comprehensive Node.js support!



Post written by Colin J. Ihrig