WebAssembly: byte-code of the future

Joshua Nussbaum - Aug 14 '23 - - Dev Community

Ever since Netscape launched JavaScript, there were some developers that liked it and others that didn't.

Regardless of which side you're on, I think we can all agree that it would be good if browsers supported more programming languages.

This is the promise of WebAssembly: Providing a generic runtime that any programming language can compile to.

Past Attempts

In the earlier days of the web, extensions were attempted with Java Applets and Microsoft ActiveX. But both were plagued by security issues and eventually dropped. The problem was they executed without access controls, which became a massive attack surface.

Later, Macromedia Flash and Silverlight had some success, but eventually met the same tragic fate. They both lacked an open standard, which made it hard for browser and OS vendors to support.

What is WebAssembly?

WebAssembly (aka WASM) is an open standard byte code format that works in all browsers. It's a low-level binary format and execution engine, conceptually similar to Oracle's JVM or Microsoft's CLR.

It's designed from the ground up to be hosted and safe. It cannot access the machine's memory or hard drive. Only the host can decide what APIs to expose.

WASM is a portable format, so it can support many programming languages. Think Rust, Ruby, Python and even JavaScript can be compiled to WASM byte code.

Though it was originally designed to target the browser, it works well outside the browser too.

It can run on the server, in the cloud, in hardware devices, or used a plugin system.

Writing WASM

There are several ways to create a .wasm file:

  • Write it by hand. (not recommended)
  • Write it with Wasm Text Format.
  • Use a higher level language like AssemblyScript, Rust, Ruby, etc.. and then compile it.

I'll show you a few examples:

Wat is WAT?

The WASM specification provides a text-based format for defining WASM modules that is called WAT (WAsm Text format). It uses S-expresions, similar to Lisp or Clojure.

Here's what a basic module looks like:

; define a module
(module
  ; define a function called "add"
  ; it takes 2 params:
  ; - $a is a 32-bit integer
  ; - $b is a 32-bit integer
  ; it returns an 32-bit integer
  (fun add (param $a i32) (param $b i32) (result $i32)
    ; load param $a onto the stack
    local.get $a

    ; load param $b onto the stack
    local.get $b

    ; perform 32-bit integer "add" operation
    i32.add

    ; the last value on the stack is returned
    ; which is the result of the `i32.add`
  )
)
Enter fullscreen mode Exit fullscreen mode

The .wat file can be compiled to a .wasm using wat2wasm which is part of the WebAssembly Toolkit CLI tools:

# outputs example.wasm
> wat2wasm example.wat
Enter fullscreen mode Exit fullscreen mode

Now the .wasm can be executed from any host. It can even be executed from the command line using wasmtime:

# invoke "add" function, and pass args 1,2
> wasmtime example.wasm --invoke add 1 2
3
Enter fullscreen mode Exit fullscreen mode

AssemblyScript

There is also a higher-level language called AssemblyScript. It's like TypeScript for WebAssembly.

If we re-write the add() function from the previous section with AssemblyScript, it would look like this:

// in add.ts
export function add(a: u32, b: u32): u32 {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

As you can see, it's much more readable now.

To compile it, use AssemblyScript's compiler asc:

pnpm install -D assemblyscript
pnpm run asc add.ts --outFile=math.wasm
Enter fullscreen mode Exit fullscreen mode

Comparing formats

To compare AssemblyScript to WAT, I built a little tool:

https://assemblyscript-play.vercel.app

You can also use the CLI wasm2wat to compare formats:

# outputs .wat format
wasm2wat math.wasm
Enter fullscreen mode Exit fullscreen mode

Runtime execution

Just like there are many ways to compile wasm, there are many ways to execute it too.

Using WebAssembly in the browser

To use the WebAssembly API in the browser, first load the assembly:

// fetch .wasm file
const response = fetch('/path/to/some.wasm')

// instantiate module with streaming
const module = WebAssembly.instantiateStreaming(response)
Enter fullscreen mode Exit fullscreen mode

Then, call one of the exported functions:

const result = module.instance.exports.add(1, 2)
Enter fullscreen mode Exit fullscreen mode

An optional API can be passed into the module too:

// fetch .wasm file
const response = fetch('/path/to/some.wasm')

// instantiate module and pass an api
const module = WebAssembly.instantiateStreaming(response, {
  imports: {
    // share console.log
    log: console.log
  }
})
Enter fullscreen mode Exit fullscreen mode

Using WebAssembly on the server

WebAssemblies can be executed on the server too. The API is virtually identical to the browser.

The only difference is that instead of fetching the .wasm from a server using a URL, it can be read from the disk using fs.readFile():

import fs from 'fs'

// read .wasm file
const bytes = await fs.promises.readFile('/path/to/some.wasm')

// instantiate the module
const module = WebAssembly.instantiate(bytes)
Enter fullscreen mode Exit fullscreen mode

Then, call one of the exported functions, just like we did in the browser:

const result = module.instance.exports.add(1, 2)
Enter fullscreen mode Exit fullscreen mode

It's also possible to do this from many other languages. For example rust, ruby, python or from the CLI.

Using WebAssembly in the Cloud

Another big use-case for WASM is the cloud.

It has some advantages over JavaScript cloud functions:

  1. No cold starts: The host only has to load a .wasm file instead of full app. Typical JS apps have many files to load, which takes a long time.
  2. Faster deploys: All that gets uploaded is a simple binary.
  3. Polyglot hosting: All languages that compile to WASM can be deployed to the cloud without requiring any special runtime.
  4. Snapshots: Execution can be snapshotted. For example, an app that does expensive work during initialization can be snapshotted. Then future requests can start with the snapshot, eliminating the expensive startup time.

A great example of WASM in the cloud is Fermyon. It's like AWS Lambda but for WebAssembly.

To use Fermyon, install their CLI spin.

Then create a new project:

# create a new spin project
# template is "http-js"
# project name is "spin-example"
spin new http-js spin-example
cd spin-example

# install dependencies
npm install
Enter fullscreen mode Exit fullscreen mode

Then define a handler in src/index.js:

const encoder = new TextEncoder()

export async function handleRequest(request) {
  return {
    status: 200,
    headers: { "content-type": "text/plain" },
    body: encoder.encode("Hello World").buffer
  }
}
Enter fullscreen mode Exit fullscreen mode

To run in dev mode:

spin watch
Enter fullscreen mode Exit fullscreen mode

To deploy to The Cloud™, run spin deploy:

spin deploy
Enter fullscreen mode Exit fullscreen mode

Notice how that deploy was instant?

Gotchas

There are still a couple rough edges of of WASM:

  • WebAssembly is still kind of new and in active development. Though it is improving rapidly.
  • Full support is not yet available for some programming languages.
  • WASM doesn't have basic data types like strings or a standard library. This is by design. Languages are expected to be provide their own standard library.
  • Because a "standard library" needs to live inside your .wasm, it can make file size large.

Most of these will be resolved with time.

The future

Over the past few years WebAssembly has made a lot of progress.

Eventually all languages will have compilation targets and runtimes for hosting it (if they don't already). This will enable all languages to run in the browser, server, or even in hardware.

It might also bring on new types of programming languages that are designed for a WebAssembly-first world.

. . . . . . . . . . . . . . . . . . .