In this blog post, we look at how WebAssembly has become an ecosystem for many programming languages and what technologies enable that.
WebAssembly is a binary instruction format for a stack-based virtual machine. Note that the abbreviation for WebAssembly is Wasm – not WASM: It is not an acronym. Key points of its design (source: WebAssembly website):
Portability: A variety of programming languages can be compiled to it and it runs on a variety of platforms.
Efficiency and speed: Wasm is a size- and load-time-efficient binary format. Wasm runtimes run it at native speed.
Safety: Wasm runs in a memory-safe, sandboxed execution environment.
Open and debuggable: Wasm has a pretty-printable textual format that can be used for “debugging, testing, experimenting, optimizing, learning, teaching, and writing programs by hand”.
Compatibility with the web platform: Since WebAssembly originated inside web browsers, it is also versionless via feature testing and backward compatible. Where available, it can access web APIs, invoke JavaScript and be invoked from JavaScript.
Most JavaScript runtimes internally use powerful compilers. asm.js is based on the observation that, by sticking to certain patterns and simple language features, JavaScript code can run at near-native speeds (usually about 50% of code natively compiled with Clang). Eventually, there was even a whole tool suite, Emscripten, that compiled LLVM-based languages with manual memory management (such as C) to asm.js.
As an example, consider the following C code:
int f(int i) {
return i + 1;
}
Compiled to asm.js, it looks like this:
function f(i) {
i = i|0;
return (i + 1)|0;
}
The bitwise operation |0
works like a type annotation and tells JavaScript engines that a given value is an int
.
Source of this section:
This is the thought process behind WebAssembly:
Keep the key benefit of asm.js: WebAssembly is a portable compilation target and part of the web platform.
Simplify the input format: WebAssembly is a binary format that is both more compact and easier to parse than JavaScript source code.
Simplify the runtime: A WebAssembly runtime only supports the much simpler WebAssembly operations. Even if an asm.js runtime only supported the asm.js subset of JavaScript, it would still be much more complex than a WebAssembly runtime.
This is what WebAssembly code looks like – represented in the WebAssembly text format WAT (source):
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add
)
(export "add" (func $add))
)
Source of this section:
Tail calls: A crucial feature for functional programming languages.
WebAssembly Garbage Collection (WasmGC): Gives Wasm access to a garbage collector – which is built into many Wasm runtimes anyway (those which also run JavaScript).
Further reading on WasmGC:
While WebAssembly originated in web browsers, it quickly became apparent that running it elsewhere would be useful, too. One technology that helps with that is the WebAssembly System Interface (WASI). Quoting its website: “[WASI] is a group of standards-track API specifications for software compiled to [Wasm]. WASI is designed to provide a secure standard interface for applications that can be compiled to Wasm from any language, and that may run anywhere—from browsers to clouds to embedded devices.”
Examples of WASI APIs:
The WebAssembly Component Model is “a broad-reaching architecture for building interoperable Wasm libraries, applications, and environments.”
A core part of WebAssembly is a module: A .wasm
file that contains functions, memory, imports, exports and more. Such modules run in Wasm runtimes and are the output when compiling to Wasm. However, such modules only support a small number of core WebAssembly types – roughly: integers and floating-point numbers. Richer types such as strings, structs and arrays have to be represented in terms of numbers – e.g., via offsets and pointers. Such representations tend not to be compatible between programming languages such as C, Rust or JavaScript.
To remedy that, the WebAssembly Component Model introduces:
A Wasm component comprises a core module plus WIT interfaces for the imports and exports of that module. Such a component is not only portable across system architectures and operating systems, but also across programming languages. In one recent experiment by Slava Kuzmich, the following was done:
Components can be linked into graphs, by connecting imports with exports. A component interacts with a runtime or other components only via imports and exports. Specifically, core modules can export Wasm memory, but components may not do so. That helps with sandboxing and interoperation between languages, especially if they use memory differently.
A WIT interface groups related functionality: It describes types and functions in the WIT language.
This is what an interface looks like (source):
interface types {
use wasi:clocks/wall-clock.{datetime};
record stat {
ino: u64,
size: u64,
mtime: datetime,
// ...
}
stat-file: func(path: string) -> result<stat>;
}
A WIT world is a contract that describes the capabilities and needs of a Wasm component. It defines:
Worlds serve two purposes:
Example of a world: WASI (the WebAssembly System Interface) defines a command
world – as part of its support for command line interfaces:
run()
for running the CLI command.This is what the corresponding WIT code looks like (source):
package wasi:cli; // package name
world command {
include imports; // included, not imported!
export run;
}
world imports {
// Included world (not shown here)
include wasi:filesystem/imports;
import stdout;
// ···
}
interface stdout {
use wasi:io/streams.{output-stream};
get-stdout: func() -> output-stream;
}
interface run {
run: func() -> result;
}
run()
and can call all of the imports.run()
.A WIT package groups related interfaces and worlds and gives them a name. It is the foundation of an ecosystem for sharing common definitions. A WIT package can be defined via multiple files. At least one of these files must specify a package name.
The previous example was a package with the name wasi:cli
. It used the type output-stream
from another package called wasi:io/streams
.
Warg is a protocol for registries for Wasm packages. A Wasm package consists of code (e.g. components) and/or interface descriptors (e.g. worlds). Work on warg is ongoing: Consult the warg website for more information.
The Warg GitHub repository contains:
Related videos:
I did a small survey to find out which Wasm languages people are using and interested in. Note that the survey was quite informal: I recruited people via Mastodon and Bluesky. That means that the results are biased towards my followers – people interested in JavaScript.
109 people submitted responses.
Language | How many people? |
---|---|
Rust | 46.79% |
Kotlin | 18.35% |
C++ | 17.43% |
WAT (WebAssembly text format) | 12.84% |
C# | 11.93% |
Go | 8.26% |
C | 6.42% |
JavaScript (embedded engine) | 5.50% |
Zig | 3.67% |
JavaScript (AssemblyScript) | 2.75% |
MoonBit | 2.75% |
Hoot (Guile Scheme) | 1.83% |
Java | 1.83% |
Python | 1.83% |
Swift | 1.83% |
C scheme | 0.92% |
Dart | 0.92% |
Elixir | 0.92% |
Encantis | 0.92% |
F# | 0.92% |
Hand-written compilers | 0.92% |
Home-grown DSL | 0.92% |
Lua | 0.92% |
Mojo | 0.92% |
OCaml | 0.92% |
Ruby | 0.92% |
Note that this excludes the programming languages that people are already using!
Language | How many people? |
---|---|
Rust | 37.61% |
Kotlin | 18.35% |
Zig | 17.43% |
C++ | 14.68% |
Python | 14.68% |
C# | 11.93% |
Go | 11.93% |
JavaScript (embedded engine) | 10.09% |
Swift | 10.09% |
WAT (WebAssembly text format) | 9.17% |
Java | 8.26% |
JavaScript (Porffor) | 8.26% |
JavaScript (AssemblyScript) | 6.42% |
MoonBit | 6.42% |
OCaml | 6.42% |
Ruby | 5.50% |
JavaScript (Static Hermes) | 4.59% |
C | 3.67% |
Grain | 2.75% |
Clojure | 0.92% |
Elixir | 0.92% |
Encantis | 0.92% |
Forth | 0.92% |
Lua | 0.92% |
Mojo | 0.92% |
PHP | 0.92% |
Raku | 0.92% |
Let’s loosely categorize the programming languages that were mentioned in this survey.
It’s remarkable how popular Rust has become as a compile-to-Wasm language. The caveat is that many of the people who follow me, like it – but it has also done well in the “State of WebAssembly 2023” survey.
Some languages were created to produce relatively low-level WebAssembly:
Some languages that existed prior to WebAssembly are getting good at compiling to Wasm: Kotlin, Zig, C++, C#, Go, Swift, Java, OCaml, C, etc.
Some interpreted languages are used with runtimes that are compiled to Wasm: Python, JavaScript, Ruby, etc.
There are projects that are compiling JavaScript to Wasm without a runtime, namely Porffor and Static Hermes.
Some new compiled languages were created with WebAssembly in mind: MoonBit, Grain, Hoot, etc.