New possibilities in the browser thanks to WebAssembly

22.09.2021 Ricky Elfner
Cloud Mobile Tech wasm Rust tutorial handson

The WebAssembly theme is code that has been specially compiled for the browser. This is in a very compact binary format. WebAssembly is a very low-level assembler-like language. Wasm has now also been defined as a web standard by the W3C. It should also be mentioned right from the start that Wasm is a supplement to JavaScript and is not intended to serve as a replacement. Since it counts as an extension, it is possible to load WebAssembly Modules within JavaScript applications and use their functions. This is also possible the other way around. So you use the flexibility of JavaScript and the performance that Wasm offers you. Because of this, this combination enables browser-enabled applications that were previously not possible.

There are several ways to use wasm, such as writing select wasm code or compiling existing code into wasm code. But more on that later.

In the past, the computing power of JavaScript alone was enough. Nowadays, this is no longer enough with applications in the area of 3D games, virtual or augmented reality, image/video editing. This problem is said to be fixed with the use of Wasm. The advantages that both worlds offer include above all:

  • in JavaScript:

    • very flexible to write web applications

    • does not require prior compilation

    • large ecosystem with many frameworks, libraries and other tools

  • at WebAssembly:

    • very compact binary format

    • is a very low-level assembler-style language

The browser VM now loads two types of code — JavaScript and WebAssembly. The WebAssembly JavaScript API makes it possible to call functions from the wasm code. This is possible because the API encloses the wasm code. Calling JavaScript functions within wasm code is also not a problem.

goals

WebAssembly primarily defines these four goals:

  • Be fast, efficient, and portable

    • wasm code can run at near native speed regardless of platform
  • Be readable and debuggable

    • although it is a low-level assembler-like language, it has a human-readable text format. This makes it possible to write, read and debug code yourself
  • Keep secure

    • wasm runs in a secure sandbox environment. It adopts the same same-origin and permission policies as the browser
  • Don't break the web

    • is designed so that wasm is backwards compatible and can therefore work with other web technologies without any problems.

Key concepts

First, if you want to use WebAssembly in your application, there are the following key concepts you should know in order to understand the structure.

Modules

A module represents a WebAssembly binary that has been compiled by the browser into executable machine code. A module, like a blob, is stateless and can be shared between windows and workers.

Memory

The memory is a resizable ArrayBuffer.

Table

A table is a resizable,typed array containing references (e.g. functions). For security reasons, these can only be saved as bytes.

instance

As soon as there is a combination of a module, memory states, tables and imported variables, it is called an instance.

The integration

Unfortunately, it is not yet possible to simply create pure wasm code using a import\ statement or the html tag <script type='module'>. The two most common options are via Fetch API or via an XMLHttpRequest.

Fetch

The fastest and also most effective way is via the function WebAssembly.instantiateStreaming(). This takes the wasm file as the first parameter. In our example, this file is loaded using the fetch command. Optionally, you can specify another parameter that provides values for the new instance.

WebAssembly.instantiateStreaming(fetch('bnvoaExample.wasm'), importObject)
.then(results => {
  // Do something with the results!
});

A promise is returned as a response, which is resolved as a ResultObject and contains two fields. The first is the module, which represents the compiled WebAssembly Module. This in turn can be instantiated again or shared via the postMessage(). The second field is instance, which contains all exported WebAssembly functions.

XMLHttpRequest

This is a slightly older variant, but is still supported. A XMLHttpRequest() must first be created. In the open()\ function, the request method must now be specified, as well as the name of the wasm code. Then it is important to set the responseType to arrrayBuffer. Now the request can be sent. In a further step, the ResultObject can then be accessed in the same way as above.

request = new XMLHttpRequest();
request.open('GET', 'bnvoaExample.wasm');
request.responseType = 'arraybuffer';
request.send();

request.onload = function() {
  var bytes = request.response;
  WebAssembly.instantiate(bytes, importObject).then(results => {
    // Do something with the results!
  });
};

Convert

As mentioned above, there is a human-readable format (.wat) in addition to the binary format (.wasm). If you only have one of the two formats, you can convert it to the other format using the wabt tool.

To do this, first install wabt:

brew install wabt

Now you have two tools available to convert your code: wasm2wat and wat2wasm.

wat2wasm test.wat -o test.wasm

wasm2wat test.wasm -o test.wat

Using

Now we will show you how to create wasm code yourself. Since we covered Rust last week, we also want to show you how to convert a Rust application into wasm code so that you can use it in your browser.

If you don't already have Rust installed, you can read more here. Next you have to install the wasm-pack via the package manager cargo. With the help of this package it is possible to compile your code to wasm code. It also provides the correct packaging to use within the browser.

cargo install wasm-pack

If you don't already have any applications in Rust that you want to compile, you must first create a Rust project.

 cargo new --lib hello-bnova
     Created library `hello-bnova` package

You should then have the following folder structure.

.
└── hello-bnova
    ├── Cargo.toml
    └── src
        └── lib.rs

To demonstrate the functionality of WebAssembly, we will add two different functions. Calling JS code within Rust code and vice versa. In our case, you have to adapt the lib.rs file as follows:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    pub fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

On line 1 the wasm_bindgen::prelude module is imported. This will be responsible for communicating between JS and Rust.

The code from line 3 to line 6 is the call to the JavaScript function alert(). The code from line 8 to line 10 again shows the possibility of calling this Rust function from JavaScript.

In order to compile the code for WemAssembly as well, you have to adapt your Cargo.toml according to the following pattern:

[package]
name = "hello-bnova"
version = "0.1.0"
authors = ["Ricky Elfner ricky.elfner@b-nova.com"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

Now you are ready to build the package for it. This process can take a little while.

wasm-pack build --target web

This process compiles your Rust code into WebAssembly. A JavaScript file is created around the WebAssembly file as a module so that the browser can use it. Furthermore, a pkg folder is created into which the JavaScript file and the WebAssembly file are moved. A matching package.json is also created from the Cargo.toml. If you already have a README.md, it will also be copied to this folder.

.
├── Cargo.lock
├── Cargo.toml
├── pkg
│   ├── hello_bnova.d.ts
│   ├── hello_bnova.js
│   ├── hello_bnova_bg.wasm
│   ├── hello_bnova_bg.wasm.d.ts
│   └── package.json
├── src
│   └── lib.rs
└── target

However, the folder target is too large to display here.

In order to be able to use your project in the browser, you have to create a index.html file, which includes your script. To do this, create the following HTML file in your root\ directory:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>hello-wasm example</title>
  </head>
  <body>
    <script type="module">
      import init, {greet} from "./hello_bnova.js";
      init()
        .then(() => {
          greet("WebAssembly")
        });
      </script>
  </body>
</html>

Now you can start a local web server, for example with python3 -m http.server and access your index.html via your browser. You should now see a notification window with the message "Hello, WebAssembly!".

Use NPM

Another option is to use a Rust application using NPM. The requirements for this are simply that you have Node.js and Npm installed. First change the root directory of your application and call the following command:

wasm-pack build --target bundler

You will get the following output:

pwd
/Users/relfner/Development/techhub/wasm/hello-bnova
Ricky-MBP:hello-bnova relfner$ wasm-pack build --target bundler
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
    Finished release [optimized] target(s) in 0.04s
[WARN]: ⚠️   origin crate has no README
[INFO]: License key is set in Cargo.toml but no LICENSE file(s) were found; Please add the LICENSE file(s) to your project directory
[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional field missing from Cargo.toml: 'repository'. This is not necessary, but recommended
[INFO]: ✨   Done in 0.55s
[INFO]: 📦   Your wasm pkg is ready to publish at /Users/relfner/Development/techhub/wasm/hello-bnova/pkg.

In order to make this package available for other JavaScript packages as well, you have to switch to the pkg\ folder and run a npm link.

cd pkg
npm link

Now you have a npm package written in Rust but compiled to WebAssembly.

Now you have to switch back to the root directory, create a new folder and switch back to it. Running npm link will create a new folder named node_modules.

cd ..
mkdir site
cd site
npm link hello-bnova

Additionally, you have to create a package.json and a webpack.config.js.

Various dependencies are defined in the first file:

{
  "scripts": {
    "serve": "webpack-dev-server"
  },
  "dependencies": {
    "hello-bnova": "^0.1.0"
  },
  "devDependencies": {
    "webpack": "^4.25.1",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.10"
  }
}

In the webpack javascript file you need to add the following

const path = require('path');
module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.js",
  },
  mode: "development"
};

Now you need a JS file that calls the function from your WebAssembly file.

import("./node_modules/hello-bnova/hello_bnova.js").then((js) => {
  js.greet("WebAssembly with NPM");
});

Since it is now also necessary to have an HTML page that embeds your previously created JavaScript, you also need an index.html file.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>hello-wasm example</title>
  </head>
  <body>
    <script src="./index.js"></script>
  </body>
</html>

Now you have almost completed all the steps to launch your Rust application in your browser. Just install all dependencies and start the server.

npm install
npm run serve

Now you can call up your page in the browser and your message will be displayed in an alter box.

Now we have shown you the basics of how to use WebAssembly. However, what is possible with the combination of JavaScript and WebAssembly and where it can be used will become a very exciting topic in the near future, which we at b-nova will definitely be following.


This text was automatically translated with our golang markdown translator.

Ricky Elfner - thinker, survivor, gadget collector. He is always on the lookout for new innovational potentials, as well as tech news, so that he can always write about current topics.