简介
Webassembly
是一种可以在浏览器端运行二进制格式代码的技术,他的目标则是想提供接近Native code
的执行效率的技术体验。 相较于文本类型的Javascirpt
而言,它拥有更小的体积,更短的加载时间,和更好的执行性能等特点 。Webassembly
允许你使用Rust
或者C/C++
等静态语言来编写,并生成目标文件后缀为wasm
的二进制格式文件。通过Fetch
或者Ajax
与Webassembly
提供的 API ,我们可以实现Javascipt
与wasm
模块的混用。
asm.js
与 wasm
相信很多人跟我一样有些疑问,asm.js 和wasm
的关系,asm.js
是Mozila
工程师ALON ZAKAI
提出的一种将静态语言编译为javascript
的一种解决方案,这里有官方的PPT。而 asm.js 则实际上是 Javascript 的一个子集,通过在已有Javascript
语法特上性进行可行的提前优化和性能改进(例如强制类型的一致性、手动的内存管理),从而达到编译器对Javascript
代码提前优化的目的。所以,asm.js
实际上是一种针对Javascript
编译器进行优化过的Javascript
文本代码,而wasm
则是浏览器直接支持的一种二进制格式文件,所以在加载速度上,文件体积上,执行效率上有更多优势。编写符合规范的asm.js
代码,通过Emscripten编译工具来将静态语言编译为asm.js
目标即可。
c
语言源程序
int f (int var) {
return var + 1;
}
编译后的asm.js
目标文件
// 通过`|0`提前声明变量和函数的返回类型。
function f(i) {
i = i|0;
return (i + 1)|0;
}
C/C++
文件编译为 asm.js
目标文件的过程
注:LLVM 为底层虚拟机(Low Level Virtual Machine)的缩写,一种用 C++编写的, 可以用于优化任何静态语言(例如 Java,Go, Rust, Swift)的底层编译器基础技术。
具体的编写和编译asm.js
的方法可以查看 Emscripten 官方相关教程。
wasm
文件与 Javascript
生成机器码的区别
对比 2 种文件生成机器码的流程会发现,Javascript
文件生成机器码需要经过语法解析,代码优化,最后才转换成机器码等过程,而wasm
的优势是本身就是通过编译器并优化过后的二进制文件,可以直接转换为机器码,省去了Javascript
需要解析,优化的工作,所以在加载和执行上本身就具有优势。接下来我们尝试用C/C++
写一个wasm
模块。
用 C/C++
编写一个 wasm
模块
搭建和安装 wasm 编写环境的步骤这里就不写了,具体可以查看官方, 这里我会编写一个模块,然后通过浏览器浏览运行结果。在这里我们利用递归算法,编写一个阶乘计算的模块factorial.c
,具体代码如下:
#include <stdio.h>
long factorial(int num) {
if (num <= 0) return 1;
else {
return num * factorial(num - 1);
}
}
int main () {
int num = factorial(10);
printf("The Result: %d \n", num);
}
执行gcc factorial.c
命令,生成a.out
文件,执行./a.out
,输出The Result: 3628800
, 测试成功。
编译factorial.c
为 wasm
模块
emcc
命令本身支持多重级别的优化编译选项(-O0 (no optimization), -O1, -O2, -Os, -Oz, and -O3)
,这里我们使用如下命令:
emcc -o test.html factorial.c -o3 -s WASM=1
- 通过
-o test.html
指定Emscripten
生成运行wasm
模块的html
文件 -o3
指定优化选项,适合发布构建-s WASM=1
指定Emscripten
输出格式为wasm
,默认打包为asm.js
文件
执行后会生成如下文件:
test.html
编译并实例化 test.wasm 模块,并在浏览器展示test.js
是C
语言模块与Javascript/wasm
文件(test.wasm)之间进行转换通信的中间代码test.wasm
二进制的wasm
模块代码
在浏览器中打开test.html
文件,即能看到展示结果:
可以看到是一个比较粗糙的展示界面。
wasm
与 Javascript
模块混用
在上面的示例中,我们编写了一个C
模块,接下来我们希望在 JS 中调用factorial
方法,想要在浏览器客户端使用wasm
模块,与 JS 模块一样,我们需要先加载,再执行。
加载 wasm
模块
由于WebAssembly
暂时并不能支持类似于通过<script type="module">
或者 ES6 import
来声明引入,所以目前的方式是利用 Fetch 或者 Ajax 的方法来加载,结合WebAssembly.instantiate()
API 来实例化加载过来的wasm
二进制代码来实现的。示例如下:
// Fetch
fetch('simple.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results => {
// Do something
});
// Ajax
request = new XMLHttpRequest();
request.open('GET', 'simple.wasm');
request.responseType = 'arraybuffer';
request.send();
request.onload = function() {
var bytes = request.response;
WebAssembly.instantiate(bytes, importObject).then(results => {
// Do something
});
};
调用 C/C++
中的方法
以上只是通过 Fetch API 获取 wasm 文件的方法,想要在 JS 中调用 C 文件里面的方法,我们需要重新打包下factorial.c
源文件
emcc factorial.c -o3 -s WASM=1 -s ONLY_MY_CODE=1 -s EXPORTED_FUNCTIONS="[’_factorial’]" -o factorial.js
- -s ONLY_MY_CODE=1 仅仅打包源文件的代码,阻止包含部分 Emscripten 的标准库
- -s EXPORTED_FUNCTIONS="[’_factorial’]" 指定导出方法(注意:这里的方法名称不加下划线会报错)
与上面的打包示例一样, 执行完命令会生成对应的factorial.wasm
和factorial.js
文件,这里我们只需要 wasm 文件即可。JS 端完整的调用代码:
// 内存管理
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 });
// WebAssembly实例对象的环境配置
const importObj = {
'global': {},
env: {
abortStackOverflow: () => { throw new Error('overflow'); },
table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' }),
tableBase: 0,
memory: memory,
memoryBase: 1024,
STACKTOP: 0,
STACK_MAX: memory.buffer.byteLength,
}
};
var CModule;
fetch('factorial.wasm', { credentials: 'same-origin' }).then(res => {
return res.arrayBuffer()
}).then(bytes => {
console.log('bytes:', bytes)
// 利用WebAssembly.instantiate接口将wasm模块的方法与importObject进行映射
return WebAssembly.instantiate(bytes, importObj)
}).then(obj => {
console.log('obj:', obj)
// 执行调用factorial
CModule = obj.instance.exports;
})
function factorial() {
var num = document.getElementById('Input').value;
var val = CModule._factorial(num)
document.getElementById('Dispaly').innerHTML = `结果:${val}`;
}
html 部分代码:
<div style="width: 200px; margin: auto; margin-top: 20px;">
<h2>阶乘计算</h2>
<input type="number" id="Input"/>
<p id="Dispaly"></p>
<button onclick="factorial()">计算</button>
</div>
以上完整的代码示例:请看
其他编写 wasm
的方法
如果你实在不想用 C/C++来编写的话,实际上目前有多种编写 wasm 的方案,可以配合 Webpack 一起使用。目前我收集了一些方式:
- Rust(Yew) Yew is a modern Rust framework inspired by Elm and ReactJS.
- AssemblyScript - Typescript
- Walt - JavaScript-like syntax
其中Yew支持在 Rust 代码中直接编写 HTML 标签,官方示例的代码是这样的:
html! {
<section class="todoapp",>
<header class="header",>
<h1>{ "todos" }</h1>
{ view_input(&model) }
</header>
<section class="main",>
<input class="toggle-all",
type="checkbox",
checked=model.is_all_completed(),
onclick=|_| Msg::ToggleAll, />
{ view_entries(&model) }
</section>
</section>
}
更多的用法可以去Yew
项目首页看看。
能用 Javascript 打包成 wasm 吗
相信很多人看到这里会问,作为一个主要开发语言是 Javascript 的开发者,当然希望通过一种语言就能完成开发工作,而且 既然 Javascript 代码最后转换为机器码,中间有那么多步骤,现在开发大部分都是用 Webpack 打包,何不写个编译器,直接把 JS 打包编译成 wasm 不就好了?
理论上当然是可以的,所以才有上面列举的类 JS 语法的编译器Walt - JavaScript-like syntax, 但是如果说想要完全使用 wasm 替代现有的 Javascript,目前来讲不现实,也没什么意义。首先 wasm 的设计目标并非是取代 Javascript,在刚才我们实现 C/C++ 的例子中我们可以体会到,整个使用过程的成本是相当之高的。Javascript 本身是动态脚本语言,在我们使用 Webpack + Babel 编译 JS 之前,简单的 Web 使用 JS 开发交互是十分简单的,不需要所谓的全家桶(Webpack + ReactJS + Redux), 只需要一个 jQuery , 或者原生 JS 就可以轻松完成,这无疑对开发者而言是成本更小的选择,并且像需要大量交互操作 HTML DOM 的这种事情,显然 JS 会更顺手一些。而正如我们在上面的介绍中介绍 asm.js 一样,wasm 提前编译和优化代码,并直接生成更小的二进制文件,实则在追寻更极致的性能,而这些性能好像更多是那些需要大量计算的游戏,和复杂的 Web 应用而需要的。所以实际上二者更多的是在形成一种互补关系。
另外,我们可以看看官方针对这个问题的 Issue Will there be a JS -> WASM compiler。
总结
2017 年 3 月份,WebAssembly 社区小组成员的四大代表( Chrome, Edge, Firefox, and WebKit)对 WebAssembly API 已经达成基本的共识,表示未来的主流浏览器默认都会支持 wasm 。需要搞清楚的是 wasm 技术并非是为了替代现有 Javascript 而出现的一种技术,而是为了填补 JS 本身的一些不足。例如需要大量 GPU, CPU 计算的游戏或者算法,但是对于需要大量 DOM 交互,常规的 Web 应用而言,Javascript 仍然是不不可替代的。
参考
- MDN
- Wikipedia asm.js
- Web-assembly-intro
- How JavaScript works: A comparison with WebAssembly + why in certain cases it’s better to use it over JavaScript
- WasmModule Instantiate
最后修改于 2018-01-05