iJS CONFERENCE Blog

The Status Quo
of WebAssembly

The current state of the implementation and where the journey will lead

Apr 24, 2019

WebAssembly is a relatively new binary format for in-web executable program code. This article is about the motivation behind WebAssembly, the current state of the implementation, and future extensions.

Displaying more than just static HTML pages within the browser has a long tradition. The demand for dynamic content is as old as the Internet itself. In 1995, when Netscape Navigator Version 2.02 made the language LiveScript available to the general public, the demand for more dynamic content also suddenly came a bit closer to reality. Later, LiveScript was renamed JavaScript and became the foundation of the internet as we know it today.

The development of more dynamic content then took its course. In 2005, James Garret coined the term AJAX in his essay and showed how it is possible to build even more interactive applications in combination with XML and backend logic. JavaScript became more and more popular.

Let us now take a look at the more recent past, namely the year 2010: JavaScript as runtime environment is everywhere. Web browsers are everywhere. A worldwide established standard.
This gives rise to the idea of using exactly this runtime environment for more than just websites.
A collaboration of the Mozilla Foundation with the games studio Epic resulted in the project Emscripten [1]. Emscripten is a C or rather C++ compiler that generates JavaScript as a result. With this compiler it became possible to translate the Unreal game engine completely into JavaScript and to run it within the browser. The impressive result of this was the Unreal cathedral demo.

The Epic tech demos made it abundantly clear: the JavaScript engines within the browsers are more powerful than many had thought, thanks to just-in-time compilation. And you can do much more with them than just fading in and out div tags. By using cross-compilers like Emscripten, it is possible to run existing source code in the form of C or C++ in new environments without major modifications. This opens up completely new customer segments and distribution possibilities for executable software for existing solutions and products.

The trend is clear. Websites and applications have a constantly increasing share of JavaScript, in extreme cases up to several million lines of source code. The size of the applications sometimes increases faster than the available computing power on mobile devices, or the usable network bandwidth for the download on the target device. This leads to two problems: one obvious, the other one perhaps less so.

The obvious problem is the mentioned application size, the download capacity, and network bandwidth. Admittedly, the size and long loading time can be counteracted by a clever module structure in the JavaScript. But nevertheless, the program code must be transmitted to the user and the available capacity of the radio networks is limited to the customer’s contract, and respectively it is also limited by the still sluggish broadband expansion in Germany.

The less obvious problem lies in the nature of the JavaScript runtime environment and the language. JavaScript can only be analyzed and validated in a reasonable way once it has been fully downloaded onto the target device. This is an obstructive and time consuming step. And JavaScript’s weak typing does not exactly support the JavaScript runtime environment during the execution. JavaScript can only be executed efficiently by skillfully using tiered just-in-time compilers [2]. This structure, however, often leads to unwanted warm-up phases of applications. Phases, in which the program does not yet work at optimal speed or even freezes at times.

WebAssembly 1.0

In response to the described problems, WebAssembly, which was to become a new open standard, was launched in 2015. Unlike previous approaches to native code, which were based on browser plug-ins (ActiveX) or vendor extensions (NaCl), all browser providers were involved in the specification from the start. The idea was to create a compact, secure and portable format for program code. This format should be easy to integrate into the existing browser landscape and build on the existing standards. The compact format should also be validatable and executable with maximum efficiency. Last but not least, it should be easy to integrate into existing compiler infrastructures. In March 2017 the time had come: WebAssembly 1.0 [3] was made available to the public in the form of an MVP (Minimum Viable Product).

But how exactly does WebAssembly work? WebAssembly consists of two parts. The first part is the so-called host environment, the WebAssembly runtime environment. This is an isolated (sandboxed) area with clearly defined interfaces. The WebAssembly code is executed in this environment. The syntax and semantics of this code are the specification’s second part.

Let us take a closer look at a WebAssembly example and let us write the obligatory “Hello World” together! WebAssembly code is a mixture of stack and register machine. The arguments for an operand and the result of the execution are stored on the stack. Additionally, predefined local variables are available for data storage. The code can be represented in two different ways – there is a text and a binary form. The text form is meant for us humans and should facilitate the debugging of the code. The binary form is the actual compact and transportable code that is passed to the WebAssembly host environment at runtime.

WebAssembly code is strongly typed. A very simple but extensible type system was chosen in version 1.0 deliberately. In fact, there are only four numeric types, 32- and 64-bit expressions of integers and floating-point numbers, respectively. There are no array- , struct- , or object structures of higher order. These must be generated by the WebAssembly code.

A linear storage area, the so-called memory, is available for data storage and can be used freely by the WebAssembly code. For more complex data structures, the code must have its own memory management. WebAssembly code can interact with its environment via an import and export mechanism, which is the only way to leave the WebAssembly sandbox. Functions of the host environment can be imported, so that they can also be called by the WebAssembly code. WebAssembly functions can also be exported, so that they can be called by the host environment.

The type system does not make writing a simple Hello World example easy for us. There is no string data type that we can use immediately. In order to not make the introduction too complicated, I rename the “Hello World” to a “Hello 42”.

Now let us jump into the deep end. Listing 1 shows the textual representation of our small program.

;; a comment
(module                                       ;; <1>
  (func $hello42 (result i32)                 ;; <2>
    (i32.mul                                  ;; <3>
      (i32.const 21)                          ;; <4>
      (i32.const 2))                          ;; <5>
  )
  (export "exportedhello42" (func $hello42))) ;; <6>

The WebAssembly textual representation consists of S-expressions. One module per file (line <1>) forms the topmost order unit. Modules combine functions (line <2>) into logical units. Functions have a unique name, optional arguments, and can return results. The $hello42 function of line <2> has no arguments in this example, but returns a result of type i32, a 32-bit integer. Functions consist of a list of statements (line <3>). In this example an integer multiplication of the i32 constants 21 (line <4>) and 2 (line <5>) is executed. The result is placed on the stack and is therefore the return value of the function. In order to also execute the function of line <2>, it has to be exported (line <6>). Here, the module-internal function is exported under the name exportedhello42.

A little bit of mischief is allowed at this point, because we have a text output after all: The WebAssembly program returns the number 42, which corresponds to the character * as ASCII code and can therefore serve as a placeholder for the universal answer and thus also as “Hello World!”

Although our small demo program does not include a more complex control flow, I would like to mention an important detail of the WebAssembly specification: only one structured control flow is allowed. This means that there are no go-to instructions. The control flow is implemented via so-called nested blocks and branch instructions. There are normal blocks and loop blocks in this context. Branch instructions can also be linked to conditions. Depending on the type of block that the branch instruction has as a target, a branch either means a jump to the start of the block (in case of a loop block), or to the end of the block (normal block). This control flow structure enables a single-pass validation of the binary code and thus pays for the optimization of the load time.

;; a simple infinity loop
(loop $l
  (br $l) ;; jump to the beginning of $1
) 
;; a block that will be left again immediately
(block $b
  (br_if $b  ;; conditional jump from block $b
    (i32.eq (i32.const 42) (i32.const 42)) ;; if 42 == 42
  )  
)

The WebAssembly textual representation consists of S-expressions. One module per file (line <1>) forms the topmost order unit. Modules combine functions (line <2>) into logical units. Functions have a unique name, optional arguments, and can return results. The $hello42 function of line <2> has no arguments in this example, but returns a result of type i32, a 32-bit integer. Functions consist of a list of statements (line <3>). In this example an integer multiplication of the i32 constants 21 (line <4>) and 2 (line <5>) is executed. The result is placed on the stack and is therefore the return value of the function. In order to also execute the function of line <2>, it has to be exported (line <6>). Here, the module-internal function is exported under the name exportedhello42.

A little bit of mischief is allowed at this point, because we have a text output after all: The WebAssembly program returns the number 42, which corresponds to the character * as ASCII code and can therefore serve as a placeholder for the universal answer and thus also as “Hello World!”

Although our small demo program does not include a more complex control flow, I would like to mention an important detail of the WebAssembly specification: only one structured control flow is allowed. This means that there are no go-to instructions. The control flow is implemented via so-called nested blocks and branch instructions. There are normal blocks and loop blocks in this context. Branch instructions can also be linked to conditions. Depending on the type of block that the branch instruction has as a target, a branch either means a jump to the start of the block (in case of a loop block), or to the end of the block (normal block). This control flow structure enables a single-pass validation of the binary code and thus pays for the optimization of the load time.

WebAssembly.instantiateStreaming(fetch('Hello42.wasm'), {}) // <1>
.then(function(result) {
  var module = result.module;                               // <2>
  var instance = result.instance;                           // <3>
  console.log(instance.exports.exportedhello42());          // <4>
})

The WebAssembly streaming instantiation API (line <1>) is an important part of the WebAssembly specification. It integrates the WebAssembly runtime environment into the JavaScript host environment of the browser. Via Fetch API, the compiled WebAssembly module is loaded. Due to the optimized binary structure, the module can be analyzed, validated and translated into machine language via ahead-of-time compilation in one step and during the download. The machine code is then further monitored at runtime by a just-in-time compiler, divided into hot and cold areas, and further optimized if necessary [2].

As the result of the instantiation, a module (line <2>) and an instance (line <3>) are returned. A module is a template from which executable instances can be created. The created instance can now be called. In this example, we call the exported function exportedhello42 (line <4>) and output the result via JavaScript console.

This very simple example already shows the great advantages of WebAssembly. There is a built-in module system that summarizes compact binary code and communicates via clear interfaces, and the compilation process is multi-stage. A program becomes optimized binary code that can be efficiently transmitted, validated, and executed. On the target platform, further just-in-time compilations allow adjustments to be made that are specifically adapted to the architecture of the target device. This could include further optimization of the binary code in regards to register usage, processor execution pipeline, cache peculiarities, or also extended heuristics for optimizing the power consumption of a program on mobile devices in order to conserve battery power.

More complex applications and high-level languages

The WebAssembly text and binary code are readable, but they are not intended for direct use by a developer. Both dialects do not exactly facilitate the efficient program writing, they are too low-level for that. WebAssembly is primarily intended as a compile target for a high-level language. High-level languages allow much more efficient programming by better abstraction of complex data types like strings, structures, or objects.

As mentioned initially, there is the Emscripten project. Emscripten is a C/C++ compiler that can also generate WebAssembly code; the whole thing is also available as a command line tool. In addition to the command line, you can also translate to JavaScript with Emscripten. We thereby have a JavaScript based C++ compiler, which runs in the browser and also supports WebAssembly as a compile target. The complete process was already implemented in the project WebAssembly Studio [5]. This is a very good starting point for further tests. Let us go back to the Hello World example, but this time correctly. Listing 4 shows the corresponding C program.

#include <stdio.h>
#include <sys/uio.h>

#define WASM_EXPORT __attribute__((visibility("default"))) // <1>

WASM_EXPORT
int main(void) {
  printf("Hello World\n");
}

/* External function that is implemented in JavaScript. */
extern void putc_js(char c); // <2>

/* Basic implementation of the writev sys call. */ 
WASM_EXPORT
size_t writev_c(int fd, const struct iovec *iov, int iovcnt) {
  size_t cnt = 0;
  for (int i = 0; i < iovcnt; i++) {
    for (int j = 0; j < iov[i].iov_len; j++) {
      putc_js(((char *)iov[i].iov_base)[j]);
    }
    cnt += iov[i].iov_len;
  }
  return cnt;
}

The main function (Listing 5) probably speaks for itself. The interesting parts of this C code are the #define WASM_EXPORT (Line <1>) and the extern function putc_js (Line <2>, Listing 6).
At these points we come into contact with the WebAssembly module system.

WebAssembly runs in a sandbox. In order to call the main function, it must be exported from a module. The corresponding declaration is generated by the compiler. Furthermore, there must also be a way for the program to write on the console. This is not possible directly from the sandbox, but only by calling an imported function. This function is implemented in JavaScript, because the console object is available there.

(func $main (export "main") (result i32)
;; program code omitted for clarity
)
let s = "";
putc_js: function (c) {
  c = String.fromCharCode(c);
  if (c == "\n") {
    console.log(s);
    s = "";
  } else {
    s += c;
  }
}

The imports and exports are resolved during the WebAssembly instantiation. The validation of the code also checks whether all required imports exist. The JavaScript code shown in Listing 7 schematically shows how the instantiation takes place in conjunction with the Hello World C code.

WebAssembly.instantiateStreaming(fetch('main.wasm'), {
  env: {                            // <1>
    putc_js: function (c) {         // <2>
      c = String.fromCharCode(c);
      if (c == "\n") {
        console.log(s);
        s = "";
      } else {
        s += c;
      }
    }
  }
}).then(function(result) {
  var module = result.module;
  var instance = result.instance;
  instance.exports.main();          // <3>
})

Imports reference a module name and an element name. In line <1> the module env is defined, which is expected by the Emscripten code. In this module, the already shown JavaScript implementation for the console output is stored under the name putc_js. When the instantiation is completed, the main function exported from the module is called in line <3>.

WebAssembly Studio is a good playground for the further steps into the development with WebAssembly. Therefore, I invite anyone who is interested to continue experimenting with this project. In addition to C code, you can also work with Rust code, which shows that even well-known high-level languages can be compiled to WebAssembly. There are WebAssembly compilers [6] for almost all common high-level languages.

What I deliberately omitted

The WebAssembly specification is much more extensive than I can show in this short article. I deliberately omitted the whole memory management part (more on this later). I also omitted the mapping of more complex data structures like arrays or objects in the linear memory, as this depends very much on the language used and the compiler implementation. This area of topics also includes language features such as virtual, overloaded, and overwritten functions within object hierarchies. In an ordinary case, the compiler should take on these details. Interested parties are welcome to have a look at the generated WebAssembly code in textual form. At the latest, when it comes to debugging WebAssembly code, knowledge about the details behind the scenes is very valuable and helpful.

Areas of application, pitfalls and planned extensions

WebAssembly is often presented as a replacement or successor of JavaScript. I rather tend to see it from the perspective of two brothers who are working together to solve certain tasks. Each of the brothers has skills that the other does not. Together they make a great team with incredible potential.

Would I use WebAssembly for simple form validation? Probably not, it is much easier and more performant with JavaScript. Would I use WebAssembly for face recognition in a live video stream?
I would definitely say yes, because there are very good C libraries such as OpenCV [7] that can be translated to WebAssembly and can therefore also be used within an interactive web page.

The performance factor and the actual benefit depend very much on how often the bridge between the WebAssembly host environment and the WebAssembly instance must be crossed by using export and import mechanics. Data type conversions and security checks take place at each crossing. This takes time. Using image processing as an example, it would be absolutely negative if the image data were stored in a JavaScript array and crossed the bridge from WebAssembly to access each individual pixel. To speed up factors, the approach is to write a complete image directly into the WebAssembly memory and call WebAssembly only once to then read the entire processing result from memory. With this approach, the described bridge is crossed only once and the type conversion only matters once. If a problem or a program can be divided into exactly such task blocks, it is ideally suited for WebAssembly usage. WebAssembly can also be used in conjunction with the WebWorker API, which can significantly increase the benefits in conjunction with the task blocks.

Now let us take a look at the current state of the WebAssembly implementation and an outlook. The version 1.0 MVP is already usable as a compile target for compact, transportable and fast executable binary code. As is the case with an MVP, features that were not absolutely necessary were omitted in the first version; they are being delivered currently though. The specification and development pipeline includes support for native threading [8] respectively thread synchronization over a shared memory area, SIMD instructions [9] for more efficient parallelization, and full support for 64-bit data types and address spaces (only 32 bits were implemented in the MVP). Also under development is the complete toolchain for source maps, debugger, and the integration into the browser’s developer tools. Features such as garbage collection [10], tail call optimization [11], or zero cost exception handling are especially important for compiler builders.

The browser’s garbage collector cannot be used in the WebAssembly MVP. The language’s runtime environment must have its own memory manager and its own GC implementation. This makes the WebAssembly module larger than it should be. The WebAssembly specification does not allow stack walking for security reasons, so even a naive mark-and-sweep garbage collector can only be implemented with detours. This problem is supposed to be solved by the GC extension. The tail call optimization should prevent the stack from overflowing, especially with recursive functions. An important point is the zero cost exception handling [12]. The WebAssembly MVP knows no exceptions, no stack unwinding, and no longjumps. Therefore, the compilers must emulate exceptions. Emscripten solves this problem by translating the functions into the so-called continuation passing style. The unwinding stack is maintained on the WebAssembly host page, i.e. via JavaScript. This is very slow and time-consuming, which is why the support for exceptions in the compiler must first be explicitly switched on. A clean zero-cost exception handling implementation in WebAssembly is not easy and the currently planned proposal – already the second one on this topic – looks very promising.

A conclusion

Personally, I find the WebAssembly MVP to be very well executed. Even though the specification has a few rough edges, it clearly shows the potential of a binary format that is natively understood by all browsers. It is particularly important for the entire entertainment industry where the size and runtime requirements for programs are particularly high. This format is no less important for the distribution of existing and new software. An AutoCAD [13] that runs in the browser prevents the installation of complex applications on the user’s PC directly. The entire patch management is made much easier with distribution via the Internet. A last use case might be the obfuscation associated with the binary format. This is a disaster for debugging, but it may be desired for some use cases. Nothing is harder to read than (almost) native source code. Powerful decompilers are not yet in sight. If you want to hide your program code, you can do this very easily with WebAssembly. The hurdle for reengineering is thereby raised clearly.

With a little wink while writing the last sentence, I want to encourage all readers to open WebAssembly Studio and to take the first steps. The standard is available, usable, and offers a lot of potential. Thanks for your attention, and Happy Coding!


Links & Literature:

[1] https://kripken.github.io/emscripten-site/

[2] https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/

[3] https://webassembly.org/

[4] https://github.com/WebAssembly/wabt

[5] https://github.com/wasdk/WebAssemblyStudio

[6] https://stackoverflow.com/questions/43540878/what-languages-can-be-compiled-to-web-assembly-or-wasm

[7] https://hacks.mozilla.org/2017/09/bootcamps-webassembly-and-computer-vision/

[8] https://github.com/WebAssembly/threads

[9] https://github.com/WebAssembly/simd

[10] https://github.com/WebAssembly/gc

[11] https://github.com/WebAssembly/tail-call

[12] https://github.com/WebAssembly/exception-handling

[13] http://blogs.autodesk.com/autocad/autocad-web-app-google-io-2018

Sign up for the iJS newsletter and stay tuned to the latest JavaScript news!

 

BEHIND THE TRACKS OF iJS

JavaScript Practices & Tools

DevOps, Testing, Performance, Toolchain & SEO

Angular

Best-Practises with Angular

General Web Development

Broader web development topics

Node.js

All about Node.js

React

From Basic concepts to unidirectional data flows