Any application that can be compiled to WebAssembly, will be compiled to WebAssembly eventually.

-- Ending's law

JavaScript诞生起到现在已经成为最流行的编程语言(之一),背后正是由Web及相关技术发展所推动的。JS应用正在变得越来越复杂(各种前后端技术、扩展到了手机APP、桌面),但这也暴露出了JS的问题:

  • 语法灵活导致开发大型项目困难
  • 性能在一些场景下不如C/C++等其他语言

一些公司研发了各种框架/语言/工具来尝试补救,比较有名的有微软的TypeScript(强类型,提升代码健壮性)、Google的Dart(新的虚拟机直接运行Dart提升性能),Firefox的asm.js(JS 的子集,引擎针对性能优化)。

那么,WebAssembly,于2015年诞生,2018年发布了1.0版本,被预言为未来的标准,现在已经被4大浏览器(FF,Chrome,Safri,Edge)支持的技术,到底是什么呢?


WebAssembly介绍

WebAssembly是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++等语言提供一个编译目标,以便它们可以在Web上运行。它也被设计为可以与JavaScript共存,允许两者一起工作。(MDN官方定义

简单来说,WebAssembly不是一门编程语言,而是一份字节码标准。WebAssembly字节码是一种抹平了不同CPU架构的机器码,WebAssembly字节码不能直接在任何一种CPU架构上运行,但由于非常接近机器码,可以非常快的被翻译为对应架构的机器码,因此WebAssembly运行速度和机器码接近。(和Java bytecode一个思路)

目前能编译成 WebAssembly 字节码的高级语言有:

  • AssemblyScript : 语法和TypeScript一致
  • c\c++ : 大量现成的库,后面的例子里面我们会用C
  • Rust
  • Kotlin : 文档;

用WebAssembly结合C和JS

其实使用WebAssembly最简单的方式是用TS写AssemblyScript(非常简单,只要注意类型的限制就行)。但是出于利用现有C库的考虑,我在下面例子里会使用WebAssembly把C和JS结合在一起。

安装

参见https://webassembly.org/getting-started/developers-guide/ ,我的工作环境是win10的WSL Ubuntu

> git clone https://github.com/emscripten-core/emsdk.git
> cd emsdk
> ./emsdk install latest
> ./emsdk activate latest
> source ./emsdk_env.sh --build=Release //每次打开新命令行都要跑,或者把内容复制到你的bashrc里

编译

我们先用C语言写一个简单的fibonacci数量计算:

#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int fib(int n) {
  int i, t, a = 0, b = 1;
  for (i = 0; i < n; i++) {
    t = a + b;
    a = b;
    b = t;
  }
  return b;
}

这里的emscripten.h是emsdk提供的头文件,EMSCRIPTEN_KEEPALIVE标记的函数,在编译的时候不会被treeshake.

我们创建一个项目,把这段C代码保存到在/c/fib.c

我们尝试把这段代码编译:

> emcc ./c/fib.c -o ./out/fib.js
> ll out

我们看到编译出了两个文件,fib.js(胶水代码)、fib.wasm(字节码)。当然,这两个文件有点大,94k和22k,那是因为我们没有启用编译优化,我们可以加个参数编译:

> emcc -O3 ./c/fib.c -o ./out/fib.js

里面的-O3代表优化级别(数字越大,优化级别越高),再ll看下,fib.js是12k,fib.wasm是85个字节

NodeJS调用wasm

为了方便演示不同调用方式,我们把刚才的编译命令加个选项,暴露出ccall和cwrap

> emcc -O3 ./c/fib.c -o ./out/fib.js -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'

从nodejs调用wasm是非常简单的,创建一个js文件直接引用生成的js,有三种调用方式:

const em_module = require('./out/fib');

em_module.onRuntimeInitialized = () => { //确认runtime初始化完毕
    console.log("--      call with _   --");
    console.log(em_module._fib(12)); // 如果不care类型安全,直接加_调用
    console.log("--      ccall         --");
    console.log(em_module.ccall("fib", 'number', ['number'],[12])); // 用ccall定义入参和出参
    console.log("--      cwrap         --");
    let fib = em_module.cwrap("fib", 'number', ['number']); // 用cwrap得到函数
    console.log(fib(12));
};

Web调用wasm

两种方式:1.使用生成的胶水代码:

<script src="/out/fib.js"></script>
<script>
  Module.onRuntimeInitialized = _ => {
    //三种调用方式:加_直接调用、ccall、cwrap
    const fib = Module.cwrap('fib', 'number', ['number']);
    console.log(fib(12));
  };
</script>

2.或者更好,在支持WebAssembly的现代浏览器上,直接load字节码wasm文件:

<script>
 (async () => {
    const response = await fetch('/out/fib.wasm');
    const buffer = await response.arrayBuffer();
    const module = await WebAssembly.compile(buffer);
    const instance = await WebAssembly.instantiate(module);
    const result = instance.exports.fib(12);
    console.log(result);
 })();
</script>

如果你的web server正确配置了MIME type:Content-Type: application/wasm的话,代码可以进一步简化为:

<script>
 (async () => {
    const fetchPromise = fetch('/out/fib.wasm');
    const { instance } = await WebAssembly.instantiateStreaming(fetchPromise);
    const result = instance.exports.fib(12);
    console.log(result);
 })();
</script>

编译现有的C语言库

接下来,我们尝试调用webp(library to encode and decode images in WebP format)这个C的库。先拉下项目:

> git clone https://github.com/webmproject/libwebp

然后写个简单的版本号调用文件web.c(参考webp的API定义):

#include "emscripten.h"
#include "src/webp/encode.h"

EMSCRIPTEN_KEEPALIVE
int version() {
  return WebPGetEncoderVersion();
}

通过-I参数把整个项目所有的代码都指给编译器:

> emcc -O3 -I libwebp webp.c libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' -o ./out/webp.js

浏览器里调用生成的wasm:

<script src="/out/webp.js"></script>
<script>
  Module.onRuntimeInitialized = _ => {
    const api = {
      version: Module.cwrap('version', 'number', []),
    };
    console.log(api.version());
  };
</script>