Getting Started With WebAssembly

Getting Started With WebAssembly

What Is WebAssembly?

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

WebAssembly is a new type of code that can be run in modern web browsers — it is a low-level assembly-like language with a compact binary format that runs with near-native performance and provides languages such as C/C++, C# and Rust with a compilation target so that they can run on the web. It is also designed to run alongside JavaScript, allowing both to work together.

How Does It Work

Who Are Using WebAssembly?

  1. Google: Using WebAssembly technology to make Gmail faster, and Squoosh, an image compression tool produced by their ChromeLab team, also compiles several common image codecs into WebAssembly for efficient image compression;
  2. Mozilla: Developing WebAssembly as a new generation of Web technology;
  3. Unity: Use WebAssembly to build a high-performance game engine;
  4. AutoCAD: Using WebAssembly to implement CAD applications that run on the Web;
  5. SketchUp: Using WebAssembly to implement a 3D modeling application that runs on the Web;
  6. Figma: A collaborative design tool that runs on the Web using WebAssembly;
  7. Dropbox: Building a client-side accelerator with WebAssembly;
  8. Microsoft: Using WebAssembly to accelerate JavaScript in the Edge browser;
  9. TensorFlow.js: Use WebAssembly to run TensorFlow models in the browser;
  10. HandBrake: Build a video converter using WebAssembly to improve performance;
  11. WordPress In Browser: Through WebAssembly, you can run this classic PHP CMS application on the browser without any server;
  12. Computer Museum in the Web Page The webmaster built a WebAssembly application that allows us to load some ancient operating systems on the page, which is very interesting.

How Do I Use It?

Through the introduction of WebAssembly - WebAssembly | MDN document, we can see that it has 4 loading methods. Among them, 1 and 2 will directly return a Promise that generates a WebAssembly instance, and 3 and 4 will return a WebAssembly.Module, which Returns a WebAssembly instance after being executed. It can also be seen from their names: 1 and 2 are instantiated, and 3 and 4 are only compiled (not instantiated).

  1. WebAssembly.instantiate()
    The main API for compiling and instantiating WebAssembly code, returning a Module and its first Instance instance.
  2. WebAssembly.instantiateStreaming()
    Compile and instantiate a WebAssembly module directly from the streaming underlying source, returning both the Module and its first Instance instance.
  3. WebAssembly.compile()
    Compiles the WebAssembly binary into a WebAssembly.Module without instantiation.
  4. WebAssembly.compileStreaming()
    Compiles WebAssembly.Module directly from streaming underlying source code, instantiating it as a separate step.

Whether it is to initialize the Web Assembly module from Streaming or bufferSource, it is just a process of loading .wasm file, and we can choose proper loading method depends on our actual needs. After loading through the above method, we will get a WebAssembly.Instance object. Through this object we can access its internal variables and functions.

One Simple Example

Instantiating A WebAssembly Module Via WebAssembly.instantiateStreaming

Demo Page Can Be Visited In Here –>

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Simple add example</title>
</head>
<body>
<script>
WebAssembly.instantiateStreaming(fetch("add.wasm")).then((obj) => {
console.log(obj.instance.exports.add(1, 2)); // "3"
});
</script>
</body>
</html>

The .wasm file in this example is compiled from the .wat file. What is a .wat file? WAT - WebAssembly Text Format.

WebAssembly has a binary format and a text format. The binary format (.wasm) is a compact binary instruction format for a stack-based virtual machine and is designed to be a portable compilation target for other higher-level languages such as C, C++, Rust, C#, Go, Python and many more. The text format (.wat) is a human readable format designed to help developers view the source of an WebAssembly module. The text format can also be used for writing codes that can be compiled to the binary format.

add.wat
1
2
3
4
5
6
7
8
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add
)
(export "add" (func $add))
)

We can compile .wat files into .wasm binaries via wabt.
More demos can be found in webassembly-examples.

What If We Instantiate It In Another Way?

WebAssembly.instantiate() and WebAssembly.instantiateStreaming() have exactly the same result, the difference is that they accept different input parameters, use WebAssembly.instantiate() Its input parameters need to use ArrayBuffer.

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Simple add example</title>
</head>
<body>
<script>
fetch("add.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes))
.then((result) => console.log(result.instance.exports.add(1, 2)));
</script>
</body>
</html>

How Do We Get The WebAssembly Module Code?

WebAssembly binaries can be compiled from multiple languages ​​(C/C++, Rust, Assembly Script, C#…), see I want to… - WebAssembly.
Here I use C code to compile .wasm as an example.

Install The Compilation Tool Emscripten

Follow the installation steps in Emscripten Documentation. After executing

bash
1
source ./emsdk_env.sh

We can use emcc in our shell to compile .c file to .wasm file.

Writing C Code For Compilation

Still I use a+b as an example. This C file module contains only one add function, which takes two numbers as input and returns the sum of their addition.

add.c
1
2
3
4
5
6
#include <emscripten/emscripten.h>

EMSCRIPTEN_KEEPALIVE
double add(double number1,double number2) {
return number1 + number2;
}

There is a strange #include <emscripten/emscripten.h>, what is it? emscripten.h provides some public C++ APIs, see emscripten.h for details.

You can see that I added the EMSCRIPTEN_KEEPALIVE macro to the add function, which tells the compiler to keep it and export it, which allows us to access it when we access the WebAssembly instance with JavaScript.

Compile the .wasm file

Excute the following command in our shell

bash
1
emcc add.c -s WASM=1 -o add.html

The above command arguments is explained as follows:

  • emcc is the command line command for Emscripten
  • -s WASM=1 tells Emscripten to output wasm files, if this parameter is not specified, asm.js will be output by default
  • -o add.html tells the compiler to generate an HTML document named add.html to run the code, as well as the wasm module and the corresponding JavaScript glue code used to compile and instantiate wasm, so that wasm can be used in the web environment used in

After running the above command, there should be three more files in your WebAssembly directory:

  • add.wasm Binary wasm module code
  • add.js A JavaScript file containing the glue code, through which native C functions are translated into JavaScript/wasm code
  • add.html An HTML file used to load, compile and instantiate wasm code, and display the output of wasm code on the browser

Run It With JavaScript

Open the add.html file generated above with an http server. After opening the page and using devtools, we can find that the .wasm file we compiled has been automatically loaded. This is because the add.js we compiled has already generated the relevant code needed to instantiate WebAssembly for us.

We can find the instantiateAsync function which is the key to load our WebAssembly Module.

add.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function instantiateAsync(binary, binaryFile, imports, callback) {
if (
!binary &&
typeof WebAssembly.instantiateStreaming == "function" &&
!isDataURI(binaryFile) &&
!ENVIRONMENT_IS_NODE &&
typeof fetch == "function"
) {
return fetch(binaryFile, { credentials: "same-origin" }).then(
(response) => {
var result = WebAssembly.instantiateStreaming(response, imports);
return result.then(callback, function (reason) {
err("wasm streaming compile failed: " + reason);
err("falling back to ArrayBuffer instantiation");
return instantiateArrayBuffer(binaryFile, imports, callback);
});
}
);
} else {
return instantiateArrayBuffer(binaryFile, imports, callback);
}
}

In this example, add.js initializes the WebAssembly instance and puts it in a global module Module, so we can access the add function in the C module through Module.asm.add.

Implement WebAssembly In Other Applications

Through the demo above, we know that we can use JavaScript to initialize WebAssembly and use it, but there is a problem that the js glue code generated by emcc creates a global variable named Module. But what if I:

  1. Don’t want to use Module as a variable name, because I may already have this name
  2. Don’t want to load WebAssembly modules in the first place
  3. No need to use WebAssembly modules in Node.js environment
  4. Want to simplify the js glue code to achieve faster loading speed

Here are my answers:

  1. Customize our WebAssembly module name
    When compiling with emcc, add the -s EXPORT_NAME="CustomModuleName" parameter to customize the module name
  2. Loading WebAssembly modules asynchronously
    When compiling with emcc, export an ES6 JavaScript module with -o target.mjs, or modularize it with -s MODULARIZE. -s MODULARIZE can be used with -s EXPORT_NAME="CustomModuleName". After loading the js glue code, you can use CustomModuleName() to instantiate WebAssembly. Among them, CustomModuleName function returns a Promise.
  3. When I don’t need Node.js related code
    When compiling with emcc, add -s ENVIRONMENT=web, the compiled glue js code will not contain Node.js related logic.
  4. Simplify the glue js code
    When compiling with emcc, add the -O3 parameter to minify. The effect of add.c in this example is: add.js(10KB/53KB), add.wasm(72B/916B).

You can find more answers on Emscripten FAQ.

WebAssembly + Web Worker?

When we use WebAssembly for more complex calculations, in order to avoid affecting the browser rendering process, we can put it into Web Worker to run.
You can refer to Using WebAssembly with Web Workers - SitePen.

This Demo Can Be Visited In Here

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Simple add example</title>
</head>
<body>
<input id="a" type="text" />
<input id="b" type="text" />
<button id="submit">Caculate</button>
<div>The Result Is <span id="result"></span></div>
<script>
if (window.Worker) {
const myWorker = new Worker("worker.js");
document.querySelector("#submit").addEventListener("click", () => {
const a = document.querySelector("#a").value;
const b = document.querySelector("#b").value;
myWorker.postMessage({
type: "excute",
params: {
functionName: "add",
arguments: [Number(a), Number(b)],
},
});
});

myWorker.onmessage = function (e) {
console.log("Message received from worker", e);
const { params, result } = e.data;
if (params.functionName === "add") {
document.querySelector("#result").innerHTML = result;
}
};
} else {
console.log("Your browser doesn't support web workers.");
}
</script>
</body>
</html>
worker.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
importScripts("add.js");
let wasmInstance = null;
const init = async () => {
wasmInstance = (await Module()).asm;
};

init();

onmessage = async function (e) {
console.log("Worker: Message received from main script");
const { type, params } = e.data;
const { functionName, arguments } = params || {};
if (type === "excute") {
if (!wasmInstance) {
await init();
}
postMessage({ params, result: wasmInstance[functionName](...arguments) });
}
};
作者

Lynan

发布于

2023-06-19

许可协议

评论