WebAssembly as an ecosystem for programming languages

[2025-01-01] dev, webassembly
(Ad, please don’t block)

In this blog post, we look at how WebAssembly has become an ecosystem for many programming languages and what technologies enable that.

What is WebAssembly?  

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.

The history of WebAssembly  

asm.js (2013)  

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:

WebAssembly (2017)  

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:

Recent new WebAssembly features  

Further reading on WasmGC:

WebAssembly System Interface (WASI): using Wasm outside web browsers  

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:

  • Filesystem: “[An API] primarily for accessing host filesystems. It has function for opening, reading, and writing files, and for working with directories.”
  • CLI: “It provides APIs commonly available in [API] environments, such as filesystems and sockets, and also provides command-line facilities such as command-line arguments, environment variables, and stdio.”
  • HTTP: “[This API] defines a collection of interfaces for sending and receiving HTTP requests and responses.”
  • Random: “[An API] for obtaining pseudo-random data.”
  • Clocks: “[An API] for reading the current time and measuring elapsed time.”

WebAssembly Component Model: making Wasm languages interoperable  

Wasm Components  

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:

  • WIT (Wasm Interface Type): a language for defining rich types.
  • Canonical ABI (Application Binary Interface): a specification for how to represent WIT in a binary format. Quote: “ABIs are specifically concerned with data layout at the bits-and-bytes level. For example, an ABI might define how integers are represented (big-endian or little-endian?), how strings are represented (pointer to null-terminated character sequence or length-prefixed? UTF-8 or UTF-16 encoded?), and how composite types are represented (the offsets of each field from the start of the structure).”

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:

  • Kotlin code was compiled to a Wasm Component.
  • It was linked with an existing Wasm Component written in Rust.
  • The result was transpiled to JavaScript and Core Wasm.
  • The final output successfully ran in Node.js.

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.

WIT Interfaces  

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>;
}

WIT Worlds  

A WIT world is a contract that describes the capabilities and needs of a Wasm component. It defines:

  • Imports: the interfaces and functions the component depends on
  • Exports: the interfaces and functions provided by the component

Worlds serve two purposes:

  • They define components. A component can define its own world or it can refer to an existing world.
  • They define hosting environments for components: The interfaces tell components which services are provided by the host and which services it uses (if any).

Example of a world: WASI (the WebAssembly System Interface) defines a command world – as part of its support for command line interfaces:

  • This world imports interfaces that CLI commands need such as filesystem functionality and access to stdout.
  • Its only export is an interface with a function 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;
}
  • Components targeting this world must implement the function run() and can call all of the imports.
  • Runtimes supporting this world must implement all of the imports and may invoke run().

WIT Packages  

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.

Wasm package management  

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:

  • Reference implementation of Warg protocol
  • Client library
  • Server
  • Command line interface

Related videos:

What Wasm languages do people use?  

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.

What Wasm languages are people using now?  

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%

What Wasm languages are people interested in?  

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%

Observations  

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:

    • WAT (WebAssembly text format) enables humans to directly write WebAssembly code.
    • Encantis is “a fun language aimed to match Wasm semantics, but with infix syntax and a more advanced type system.”
    • AssemblyScript is a relatively low-level subset of TypeScript that produces good Wasm output.
  • 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.

Further reading