webassembly 是一种可以在浏览器端运行二进制格式代码的技术,他的目标则是想提供接近 Native code 的执行效率的技术体验。 相较于文本类型的 Javascirpt 而言,它拥有更小的体积,更短的加载时间,和更好的执行性能等特点 。Webassembly 允许你使用 Rust 或者 C/C++ 等静态语言来编写,并生成目标文件后缀为 wasm 的二进制格式文件。通过 Fetch 或者 Ajax 与 Webassembly 提供的 API ,我们可以实现 Javascipt 与 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 目标即可。
- int f (int var) {
- return var + 1;
- }
- // 通过`|0`提前声明变量和函数的返回类型。
- function f(i) {
- i = i | 0;
- return (i + 1) | 0;
- }
具体的编写和编译 asm.js 的方法可以查看 Emscripten 官方 相关教程。
对比 2 种文件生成机器码的流程会发现,Javascript 文件生成机器码需要经过语法解析,代码优化,最后才转换成机器码等过程,而 wasm 的优势是本身就是通过编译器并优化过后的二进制文件,可以直接转换为机器码,省去了 Javascript 需要解析,优化的工作,所以在加载和执行上本身就具有优势。接下来我们尝试用 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, 测试成功。
emcc 命令本身支持多重级别的优化编译选项
,这里我们使用如下命令:
- (-O0 (no optimization), -O1, -O2, -Os, -Oz, and -O3)
emcc -o test.html factorial.c -o3 -s WASM=1
执行后会生成如下文件:
在浏览器中打开 test.html 文件,即能看到展示结果:
可以看到是一个比较粗糙的展示界面。
在上面的示例中,我们编写了一个 C 模块,接下来我们希望在 JS 中调用 factorial 方法,想要在浏览器客户端使用 wasm 模块,与 JS 模块一样,我们需要先加载,再执行。
由于 WebAssembly 暂时并不能支持类似于通过
或者 ES6 import 来声明引入,所以目前的方式是利用 Fetch 或者 Ajax 的方法来加载,结合
- <script type="module">
API 来实例化加载过来的 wasm 二进制代码来实现的。示例如下:
- WebAssembly.instantiate()
- // 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
- });
- };
以上只是通过 Fetch API 获取 wasm 文件的方法,想要在 JS 中调用 C 文件里面的方法,我们需要重新打包下 factorial.c 源文件
emcc factorial.c -o3 -s WASM=1 -s ONLYMYCODE=1 -s EXPORTEDFUNCTIONS="['factorial']" -o factorial.js
与上面的打包示例一样, 执行完命令会生成对应的 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>
如果你实在不想用 C/C++ 来编写的话,实际上目前有多种编写 wasm 的方案,可以配合 Webpack 一起使用。目前我收集了一些方式:
其中 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 的开发者,当然希望通过一种语言就能完成开发工作,而且 既然 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 仍然是不不可替代的。
来源: https://juejin.im/entry/5a4f8d766fb9a01ca3251786