Webassembly 技术的探索与实践
Webassembly 是一种可以在浏览器端运行二进制格式代码的技术,他的目标则是想提供接近 Native code 的执行效率的技术体验

简介

Webassembly 是一种可以在浏览器端运行二进制格式代码的技术,他的目标则是想提供接近Native code的执行效率的技术体验。 相较于文本类型的Javascirpt而言,它拥有更小的体积,更短的加载时间,和更好的执行性能等特点 。Webassembly 允许你使用Rust或者C/C++等静态语言来编写,并生成目标文件后缀为wasm的二进制格式文件。通过Fetch或者AjaxWebassembly提供的 API ,我们可以实现Javasciptwasm模块的混用。

asm.jswasm

相信很多人跟我一样有些疑问,asm.jswasm的关系,asm.jsMozila工程师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 目标文件的过程

graph0

注:LLVM 为底层虚拟机(Low Level Virtual Machine)的缩写,一种用 C++编写的, 可以用于优化任何静态语言(例如 Java,Go, Rust, Swift)的底层编译器基础技术。

具体的编写和编译asm.js的方法可以查看 Emscripten 官方相关教程。

wasm 文件与 Javascript 生成机器码的区别

graph1

对比 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.cwasm 模块

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.jsC语言模块与Javascript/wasm文件(test.wasm)之间进行转换通信的中间代码
  • test.wasm 二进制的wasm模块代码

在浏览器中打开test.html文件,即能看到展示结果:

display_wasm

可以看到是一个比较粗糙的展示界面。

wasmJavascript 模块混用

在上面的示例中,我们编写了一个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.wasmfactorial.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 一起使用。目前我收集了一些方式:

其中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 仍然是不不可替代的。

参考


最后修改于 2018-01-05