10081677wc / blog

78 stars 6 forks source link

WebAssembly 入门实践 #23

Open 10081677wc opened 6 years ago

10081677wc commented 6 years ago

WebAssembly 入门实践

配置编译环境

以 MacOS 操作系统和 Chrome 浏览器为例:

安装 emsdk Emscripten

Emscripten is a toolchain for compiling to asm.js and WebAssembly, built using LLVM, that lets you run C and C++ on the web at near-native speed without plugins.

在浏览器中访问 chrome://flags/#enable-webassembly 并设置为 enable

打开终端并新建目录 hello

$ mkdir hello
$ cd hello

在当前目录下编写 C 程序 hello.c 用于打印字符串 “Hello World”:

$ echo '#include <stdio.h>' > hello.c
$ echo 'int main(int argc, char ** argv) {' >> hello.c
$ echo 'printf("Hello, world!\n");' >> hello.c
$ echo '}' >> hello.c

编译 C 程序为 wasm 并输出至容器 hello.html 中:

$ emcc hello.c -s WASM=1 -o hello.html

使用 emrun 命令搭建本地服务器(端口号为8080):

$ emrun --no_browser --port 8080 .

在浏览器中访问链接 http://localhost:8080/hello.html,若 Emscripten 控制台中打印字符串 “Hello World” 则配置成功。

image_1c7qegcb51kgq1gc3cgn18v1eop9.png-126.1kB

快速体验 WebAssembly

WebAssembly.compile(new Uint8Array(`
  00 61 73 6d  01 00 00 00  01 0c 02 60  02 7f 7f 01
  7f 60 01 7f  01 7f 03 03  02 00 01 07  10 02 03 61
  64 64 00 00  06 73 71 75  61 72 65 00  01 0a 13 02
  08 00 20 00  20 01 6a 0f  0b 08 00 20  00 20 00 6c
  0f 0b`.
  trim().
  split(/[\s\r\n]+/g).
  map(str => parseInt(str, 16)))).
  then((module) => {
    const instance = new WebAssembly.Instance(module)
    const { add, square } = instance.exports
    console.log('2 + 4 =', add(2, 4))
    console.log('3^2 =', square(3))
    console.log('(2 + 5)^2 =', square(add(2, 5)))
  })

打开浏览器控制台执行上述代码,如果报错则说明浏览器不支持 WebAssembly,如果没有报错则运行结果打印如下,并会返回一个 Promise 对象:

2 + 4 = 6
3^2 = 9
(2 + 5)^2 = 49

在 WebAssembly 提供的部分 JavaScript APIWebAssembly.compile 可以用来编译 wasm 的二进制源码:它接受 BufferSource 格式的参数,返回一个 Promise 对象。

把 C/C++ 编译成 WebAssembly

在上面章节中安装的工具 Emscripten 基于 LLVM,可以将 C/C++ 代码编译为 asm.js,在其 1.37 以上的版本支持使用 WASM 标志直接生成 WebAssembly 二进制文件(后缀为 .wasm 的文件),否则我们就需要使用 Binaryen 来把 asm.js 代码编译到 WebAssembly。

首先我们编写 C 代码如下,实现加法和平方函数:

int add (int x, int y) {
  return x + y;
}

int square (int x) {
  return x * x;
}

然后执行命令:

emcc math.c -Os -s WASM=1 -s SIDE_MODULE=1 -o math.wasm

我们可以使用命令 emcc --help 来学习 emcc 的选项:

生成的 wasm 文件内容如下(没错!就是上面章节中使用的 WebAssembly 二进制源码):

0061 736d 0100 0000 000c 0664 796c 696e 6b80 80c0 0200 010f 0360 027f 7f01 7f60 017f 017f 6000 0002 1301 0365 6e76 0a6d 656d 6f72 7942 6173 6503 7f00 0305 0400 0102 0206 0b02 7f01 4100 0b7f 0141 000b 0735 0412 5f5f 706f 7374 5f69 6e73 7461 6e74 6961 7465 0003 045f 6164 6400 0007 5f73 7175 6172 6500 010b 7275 6e50 6f73 7453 6574 7300 020a 2604 0700 2001 2000 6a0b 0700 2000 2000 6c0b 0300 010b 1000 2300 2401 2301 4180 80c0 026a 2402 0b

如何运行 WebAssembly 二进制文件?

目前只能使用 Javascript API 来调用 wasm 中提供的接口,简单分为四个步骤:

  1. 加载 .wasm 文件
  2. 将二进制源码转换为 ArrayBuffer 类型数组
  3. 编译至 WebAssembly.Module
  4. 实例化 WebAssembly.Module 获取可调用的接口

实现一个简易的加载函数如下:

function loadWebAssembly(filename, imports = {}) {
  return fetch(filename)
    .then(response => response.arrayBuffer())
    .then(buffer => WebAssembly.compile(buffer))
    .then((module) => {
      imports.env = imports.env || {};
      Object.assign(imports.env, {
        memoryBase: 0,
        tableBase: 0,
        memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }),
        table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' })
      });

      return new WebAssembly.Instance(module, imports);
    });
}

需要注意的是:WebAssembly.compileWebAssembly.instantiate 都是异步接口,本身会返回一个 Promise 对象;同时,在 wasm 文件里可能还会引入一些环境变量,在实例化的同时需要初始化内存空间和变量映射表,也就是 WebAssembly.MemoryWebAssembly.Table

loadWebAssembly('./math.wasm')
    .then((instance) => {
        const { _add, _square } = instance.exports;
        console.log('2 + 4 =', _add(2, 4));
        console.log('3^2 =', _square(3));
        console.log('(2 + 5)^2 =', _square(_add(2, 5)));
    });

在具有上述 math.wasm 文件和加载函数后,我们就可以正常调用导出的接口,需要注意的是,导出的函数名都默认带上 _ 前缀