What is Web Assembly?
Intro to Web Assembly#
As part of the specification of Wasm, no web specific assumptions are made, thus although the original intent was to introduce a low level language to the web, the design allows Wasm to be used in a variety of other contexts as well.
Wasm has four value types, abbreviated
These types represent 32 and 64 bit integers, as well as 32 and 64 bit floating point numbers. These floating point numbers are also known as single and double precision as defined by IEEE 754. The integer types are not signed or unsigned in the spec even. Do not be confused by the fact that the term
i32 is the Rust syntax for a signed 32 bit integer. As we will see later we can use either unsigned or signed integer types in Rust code, e.g. both
u32, with the signedness in Wasm inferred by usage.
Wasm has functions which map a vector of value types to a vector of value types:
function = vec(valtype) -> vec(valtype)
However, the return type vector is currently limited to be of length at most 1. In other words, Wasm functions can take 0 or more arguments and can either return nothing or return a single value. This restriction may be removed in the future.
The WebAssembly computational model is based on a stack machine. This means that every operation can be modeled by maybe popping some values off a virtual stack, possibly doing something with these values, and then maybe pushing some values onto this stack. Each possible operation is well-defined in the Wasm specification as to exactly how a specific opcode interacts with the stack. For example, imagine we start with an empty stack and we execute the following operations:
The first operation has two parts,
i64.const is an opcode which takes one argument, 16 in this case. Thus,
i64.const 16 means push the value
16 as an i64 constant onto the top of the stack. Similarly the second operation pushes
2 onto the stack leaving our stack to look like:
The next operation
i64.div_u is defined to pop two values off the top of the stack, perform unsigned division between those two values and push the result back onto the top of the stack. In this case the first number popped off divides the second number popped off, so at the end of this operation the stack contains:
Rust in the browser#
Rust uses the LLVM project as the backend for its compiler. This means that the Rust compiler does all of the Rust specific work necessary to build an intermediate representation (IR) of your code which is understood by LLVM. From that point, LLVM is used to turn that IR into machine code for the particular target of your choosing.
Targets in LLVM can roughly be thought of as architectures, such as
armv7. Wasm is just another target. Hence, Rust can support Wasm if LLVM supports Wasm, which luckily it does. This is also one of the ways that C++ supports Wasm through the Clang compiler which is part of the LLVM project.
Rust installations are managed with the
rustup tool which makes it easy to have different toolchains (nightly, beta, stable) as well as different targets installed simultaneously. The target triple for
wasm32-unknown-unknown. The target triples have the form
<arch>-<vendor>-<sys>, for example
x86_64-apple-darwin is the default triple for a modern Macbook. The
unknown vendor and system mean to use the defaults for the specific architecture. We can install this by running:
The Smallest Wasm Library#
Let's create a Wasm library that does absolute nothing but generate a Wasm library. We start off by creating a library with Cargo:
We need to specify that our library will be a C dynamic library as this is the type of compilation format that Wasm uses. Depending on the target architecture this is also how you would build a
.so on Linux or
.dylib on Mac OS. We do this by adding the
lib section to our Cargo.toml:
We can then remove all of the code in
src/lib.rs to leave a blank file. This technically is a Wasm library, just with no code. Let's make a release build for the Wasm target:
By default, Cargo puts the build artifacts in
target/<mode> for the default target, which in this case is
target/wasm32-unknown-unknown/release. Inside that directory, we have a
do_nothing.wasm file which is our "empty" Wasm module. But how big is it:
1.4M to do nothing! That's insane! But it turns out it is because the output binary still includes debug symbols.
Stripping out debug symbols can be done with a tool called
wasm-strip which is part of the WebAssembly Binary Toolkit (WABT). The repository includes instructions on how to build that suite of tools. Assuming you have that installed somewhere on your path, we can run it to strip our binary: