WebAssembly 101
WebAssembly 的概念、意义以及未来带来的性能提升相信已是耳熟能详,笔者在前端每周清单系列中也是经常会推荐 WebAssembly 相关文章。不过笔者也只是了解其概念而未真正付诸实践,本文即是笔者在将我司某个简单项目中的计算模块重构为 WebAssembly 过程中的总结。在简单的实践中笔者个人感觉,WebAssembly 的抽象程度会比 JavaScript 高不少,未来对于大型项目的迁移,对于纯前端工程师而言可能存在的坑也是不少,仿佛又回到了被指针统治的年代。本文笔者使用的案例已经集成到了 React 脚手架 create-react-boilerplate 中 ,可以方便大家快速本地实践。

编译环境搭建

我们使用 Emscripten 将 C 代码编译为 wasm 格式,官方推荐的方式是首先下载 Portable Emscripten SDK for Linux and OS X (emsdk-portable.tar.gz) 然后利用 emsdk 进行安装:
1
$ ./emsdk update
2
$ ./emsdk install latest
3
# 如果出现异常使用 ./emsdk install sdk-1.37.12-64bit
4
# https://github.com/kripken/emscripten/issues/5272
Copied!
安装完毕后激活响应环境即可以进行编译:
1
$ ./emsdk activate latest
2
$ source ./emsdk_env.sh# you can add this line to your .bashrc
Copied!
笔者在本地执行上述搭建步骤时一直失败,因此改用了 Docker 预先配置好的镜像进行处理:
1
# 拉取 Docker 镜像
2
docker pull 42ua/emsdk
3
4
5
6
# 执行编译操作
7
docker run --rm -v $(pwd):/home/src 42ua/emsdk emcc hello_world.c
Copied!
对应的 Dockfile 如下所示,我们可以自行修改以适应未来的编译环境:
1
FROM ubuntu
2
3
4
RUN \
5
apt-get update && apt-get install -y build-essential \
6
cmake python2.7 python nodejs-legacy default-jre git-core curl && \
7
apt-get clean && \
8
\
9
cd ~/ && \
10
curl -sL https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz | tar xz && \
11
cd emsdk-portable/ && \
12
./emsdk update && \
13
./emsdk install -j1 latest && \
14
./emsdk activate latest && \
15
\
16
rm -rf ~/emsdk-portable/clang/tag-*/src && \
17
find . -name "*.o" -exec rm {} \; && \
18
find . -name "*.a" -exec rm {} \; && \
19
find . -name "*.tmp" -exec rm {} \; && \
20
find . -type d -name ".git" -prune -exec rm -rf {} \; && \
21
\
22
apt-get -y --purge remove curl git-core cmake && \
23
apt-get -y autoremove && apt-get clean
24
25
26
# http://docs.docker.com/engine/reference/run/#workdir
27
WORKDIR /home/src
Copied!
到这里基本环境已经配置完毕,我们可以对简单的 counter.c 进行编译,源文件如下:
1
int counter = 100;
2
3
4
int count() {
5
counter += 1;
6
return counter;
7
}
Copied!
编译命令如下所示,如果本地安装好了 emcc 则可以直接使用,否则使用 Docker 环境进行编译:
1
$ docker run --rm -v $(pwd):/home/src 42ua/emsdk emcc counter.c -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm
2
$ emcc counter.c -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm
3
4
5
# 如果出现以下错误,则是由如下参数
6
# WebAssembly Link Error: import object field 'DYNAMICTOP_PTR' is not a Number
7
emcc counter.c -O1 -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm
Copied!
这样我们就得到了 WebAssembly 代码:

与 JavaScript 集成使用

独立的 .wasm 文件并不能直接使用,我们需要在客户端中使用 JavaScript 代码将其加载进来。最朴素的加载 WebAssembly 的方式就是使用 fetch 抓取然后编译,整个过程可以封装为如下函数:
1
// 判断是否支持 WebAssembly
2
if (!('WebAssembly' in window)) {
3
alert('当前浏览器不支持 WebAssembly!');
4
} // Loads a WebAssembly dynamic library, returns a promise. // imports is an optional imports object
5
function loadWebAssembly(filename, imports) {
6
// Fetch the file and compile it
7
return fetch(filename)
8
.then(response => response.arrayBuffer())
9
.then(buffer => WebAssembly.compile(buffer))
10
.then(module => {
11
// Create the imports for the module, including the
12
// standard dynamic library imports
13
imports = imports || {};
14
imports.env = imports.env || {};
15
imports.env.memoryBase = imports.env.memoryBase || 0;
16
imports.env.tableBase = imports.env.tableBase || 0;
17
if (!imports.env.memory) {
18
imports.env.memory = new WebAssembly.Memory({ initial: 256 });
19
}
20
if (!imports.env.table) {
21
imports.env.table = new WebAssembly.Table({
22
initial: 0,
23
element: 'anyfunc'
24
});
25
} // Create the instance.
26
return new WebAssembly.Instance(module, imports);
27
});
28
}
Copied!
我们可以使用上述工具函数加载 wasm 文件:
1
loadWebAssembly('counter.wasm')
2
.then(instance => {
3
var exports = instance.exports; // the exports of that instance
4
var count = exports. _count; // the "_count" function (note "_" prefix)
5
// 下面即可以调用 count 函数
6
}
7
);
Copied!
而在笔者的脚手架中,使用了 wasm-loader 进行加载,这样可以将 wasm 直接打包在 Bundle 中,然后通过 import 导入:
1
import React, { PureComponent } from 'react';
2
3
import CounterWASM from './counter.wasm';
4
import Button from 'antd/es/button/button';
5
6
import './Counter.scss';
7
8
/**
9
* Description 简单计数器示例
10
*/
11
export default class Counter extends PureComponent {
12
state = {
13
count: 0
14
};
15
16
componentDidMount() {
17
this.counter = new CounterWASM({
18
env: {
19
memoryBase: 0,
20
tableBase: 0,
21
memory: new window.WebAssembly.Memory({ initial: 256 }),
22
table: new window.WebAssembly.Table({ initial: 0, element: 'anyfunc' })
23
}
24
});
25
this.setState({
26
count: this.counter.exports._count()
27
});
28
}
29
/**
30
* Description 默认渲染函数
31
*/
32
33
render() {
34
const isWASMSupport = 'WebAssembly' in window;
35
36
if (!isWASMSupport) {
37
return <div> 浏览器不支持 WASM </div>;
38
}
39
40
return (
41
<div className="Counter__container">
42
<span> 简单计数器示例: </span>
43
<span>{this.state.count}</span>
44
45
<Button
46
type="primary"
47
onClick={() => {
48
this.setState({
49
count: this.counter.exports._count()
50
});
51
}}
52
>
53
点击自增
54
</Button>
55
</div>
56
);
57
}
58
}
Copied!
在使用 wasm-loader 时,其会调用 new WebAssembly.Instance(module, importObject);
  • moduleWebAssembly.Module 实例。
  • importObject 即默认的由 wasm-loader 提供的对象。

简单游戏引擎重构

上文我们讨论了利用 WebAssembly 重构简单的计数器模块,这里我们以简单的游戏为例,交互式的感受 WebAssembly 带来的性能提升,可以直接查看游戏的在线演示。这里的游戏引擎即是执行部分计算与重新赋值操作,譬如这里的计算下一个位置状态的函数在 C 中实现为:
1
EMSCRIPTEN_KEEPALIVE
2
void computeNextState()
3
{
4
loopCurrentState();
5
6
7
int neighbors = 0;
8
int i_m1, i_p1, i_;
9
int j_m1, j_p1;
10
int height_limit = height - 1;
11
int width_limit = width - 1;
12
for (int i = 1; i < height_limit; i++)
13
{
14
i_m1 = (i - 1) * width;
15
i_p1 = (i + 1) * width;
16
i_ = i * width;
17
for (int j = 1; j < width_limit; j++)
18
{
19
j_m1 = j - 1;
20
j_p1 = j + 1;
21
neighbors = current[i_m1 + j_m1];
22
neighbors += current[i_m1 + j];
23
neighbors += current[i_m1 + j_p1];
24
neighbors += current[i_ + j_m1];
25
neighbors += current[i_ + j_p1];
26
neighbors += current[i_p1 + j_m1];
27
neighbors += current[i_p1 + j];
28
neighbors += current[i_p1 + j_p1];
29
if (neighbors == 3)
30
{
31
next[i_ + j] = 1;
32
}
33
else if (neighbors == 2)
34
{
35
next[i_ + j] = current[i_ + j];
36
}
37
else
38
{
39
next[i_ + j] = 0;
40
}
41
}
42
}
43
memcpy(current, next, width * height);
44
}
Copied!
而对应的 JS 版本引擎的实现为:
1
computeNextState() {
2
let neighbors, iM1, iP1, i_, jM1, jP1;
3
4
this.loopCurrentState();
5
6
for (let i = 1; i < this._height - 1; i++) {
7
iM1 = (i - 1) * this._width;
8
iP1 = (i + 1) * this._width;
9
i_ = i * this._width;
10
for (let j = 1; j < this._width - 1; j++) {
11
jM1 = j - 1;
12
jP1 = j + 1;
13
neighbors = this._current[iM1 + jM1];
14
neighbors += this._current[iM1 + j];
15
neighbors += this._current[iM1 + jP1];
16
neighbors += this._current[i_ + jM1];
17
neighbors += this._current[i_ + jP1];
18
neighbors += this._current[iP1 + jM1];
19
neighbors += this._current[iP1 + j];
20
neighbors += this._current[iP1 + jP1];
21
if (neighbors === 3) {
22
this._next[i_ + j] = 1;
23
} else if (neighbors === 2) {
24
this._next[i_ + j] = this._current[i_ + j];
25
} else {
26
this._next[i_ + j] = 0;
27
}
28
}
29
}
30
this._current.set(this._next);
31
}
Copied!
本部分的编译依旧是直接将 engine.c 编译为 engine.wasm,不过在导入的时候我们需要动态地向 wasm 中注入外部函数:
1
this.module = new EngineWASM({
2
env: {
3
memoryBase: 0,
4
tableBase: 0,
5
memory: new window.WebAssembly.Memory({ initial: 1024 }),
6
table: new window.WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
7
_malloc: size => {
8
let buffer = new ArrayBuffer(size);
9
return new Uint8Array(buffer);
10
},
11
_memcpy: (source, target, size) => {
12
let sourceEnd = source.byteLength;
13
14
let i, j;
15
16
for (
17
i = 0, j = 0, k = new Uint8Array(target), l = new Uint8Array(source);
18
i < sourceEnd;
19
++i, ++j
20
)
21
k[j] = l[i];
22
}
23
}
24
});
Copied!
到这里文本告一段落,笔者最后需要声明的是因为这只是随手做的实验,最后的代码包括对于内存的操作可能存在潜在问题,请读者批评指正。
Last modified 2yr ago