After the downfall of browser plugins such as Flash Player, JavaScript became the dominant option for in-browser computation. With the introduction of Web Assembly (Wasm), it is now possible to compile code written in other languages to run in the browser.
Written by Jacob Gelman
Before exploring Web Assembly, it is important to understand what an assembly language is more generally. In simple terms, an assembly language is a low-level text-based programming language in which programs are a series of instructions understood by a particular type of CPU.
There is no single language called "assembly"; since different CPU architectures support different instruction sets, each CPU architecture needs its own assembly language. For example, modern Intel chips use the x86 architecture whereas Apple's M-series chips use ARM64, each of which has its own assembly language. An assembly program is fed into an assembler to be converted to machine code, a binary representation of the same program ready to be executed by the CPU.
In modern times, it is neither practical nor advisable to write programs in assembly for systems that do not require bare-metal performance; high-level languages allow programs to be written in a manner that is easier, safer, and more portable.
Despite its name, WebAssembly (Wasm) is not an assembly language in the traditional sense since it is not specific to a CPU architecture. According to webassembly.org:
WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.
In other words, Wasm serves as a "common ground" between various programming languages and native machine code, allowing the browser to perform the final translation.
Wasm in the browser has two primary benefits: portability and performance. Deploying programs across different platforms still remains a challenge. JavaScript is a highly portable language; the same JavaScript program can be executed in the same way in any standard web browser. WebAssembly opens up the same portability of JavaScript to other languages.
In terms of performance, JavaScript arrives at the browser in source form and lacks static typing, necessitating an initial parsing step and ongoing optimizations during runtime. In contrast, WebAssembly arrives at the browser in a binary format that can be translated into machine code and executed with minimal runtime overhead—allowing WebAssembly to vastly outperform JavaScript for certain types of tasks.
In practical applications, Wasm serves as a compilation target for other languages (e.g. C/C++, Rust, etc.) and thus isn't typically written or examined directly. However, for gaining an understanding of Wasm at the fundamental level, we will do so in this article.
WebAssembly is used to refer to both to the WebAssembly binary format (.wasm
)
and the WebAssembly text format (.wat
). The text format is used by humans to
write or examine Wasm, and the binary format is what is executed by the browser.
A Wasm module can be written by hand in a .wat
file and then converted into a
binary .wasm
file using the wat2wasm
tool from the
WebAssembly Binary Toolkit.
Let's examine a simple JavaScript module exporting a single function, f2c
,
which converts temperatures in Fahrenheit to Celsius:
Now, let's implement a functionally-equivalent module in WebAssembly and perform the same conversion.
Examine the f2c
function in convert.wat
. Since Wasm is a "stack-based
virtual machine," each instruction operates on the stack. Let's break down what
occurs during the course of execution when the function is called (assume 212 is
passed as the argument):
Instruction | Stack (bottom to top) | Description |
---|---|---|
local.get $f | [212.0] | Get value of argument $f (212.0 ), push onto stack |
f32.const 32.0 | [212.0 32.0] | Push constant 32.0 onto stack |
f32.sub | [180.0] | Pop top two values from stack, take their difference, and push result back onto stack |
f32.const 5.0 | [180.0 5.0] | Push constant 5.0 onto stack |
f32.mul | [900.0] | Pop top two values from stack, take their product, and push result back onto stack |
f32.const 9.0 | [900.0 9.0] | Push constant 9.0 onto stack |
f32.div | [100.0] | Pop top two values from stack, take their quotient, and push result back onto stack |
The value on the top of the stack is taken as the return value, so the result is 100 as expected (212°F = 100°C).