了解一下 WebAssembly 的原理。

环境准备

  • 安装 emscripten
    • Mac 可直接通过 brew 来安装:brew install emscripten
  • 安装 wabt The WebAssembly Binary Toolkit
    • Mac 可直接使用 brew install wabt 来安装。

Quick Start

新建 mian.c 文件

#include<stdio.h>

int main() {
  printf("hello world\n");
}

Run it by NodeJS

# compile c-lang
$ emcc main.c

# run code by NodeJS
$ node a.out.js
hello, world!

Run it by Browser

# compile it & default html page
$ emcc main.c -O3 -o main.html

# start a static http server
$ npx serve .

# open it
$ open http://localhost:5000/main.html

最简单的 a + b 实现

参考:https://depth-first.com/articles/2019/10/16/compiling-c-to-webassembly-and-running-it-without-emscripten/

新建 main.c

int add (int first, int second)
{
  return first + second;
}

新建 main.html

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <script type="module">
      (async() => {
        const response = await fetch('main.wasm');
        const bytes = await response.arrayBuffer();
        const { instance } = await WebAssembly.instantiate(bytes);

        console.log('The answer is: ' + instance.exports.add(1, 2));
      })();
    </script>
  </body>
</html>
# 如果报 `no available targets are compatible`,可以查看一下后面章节的常见问题
$ clang --target=wasm32 --no-standard-libraries -Wl,--export-all -Wl,--no-entry -o main.wasm main.c

# 启动静态资源服务器
$ npx serve .

# 打开页面
$ open http://localhost:5000/main.html

WebAssembly 二进制逐字解析

新建 main.wat 文件,实现一个最简单的 1 + 2 = 3 的程序,因为正常的打印输出涉及 import, 所以,为简单起见,直接 return pop 最后一个元素了。

PS: start 函数是不支持返回值的,所以外部获取不到最终的 return 结果。

(module
  (func
    i32.const 1
    i32.const 2
    i32.add
    return
  )
  (start 0)
)

使用 wat2wasm 编译上述文件,并查看最终的二进制文件。

$ wat2wasm main.wat

$ xxd -c 8 hello.wasm
00000000: 0061 736d 0100 0000  .asm....
00000008: 0104 0160 0000 0302  ...`....
00000010: 0100 0801 000a 0a01  ........
00000018: 0800 4101 4102 6a0f  ..A.A.j.
00000020: 0b                   .

字节码与指令对应分析,参考 WebAssembly Opcodes, learning-webassembly-2-wasm-binary-format

地址 1 2 3 4 5 6 7 8
00000000 00 61 73 6d 01 00 00 00
备注 asm 文件类型标记 版本标记
00000008 01 04 01 06 00 00 03 02
备注 类型区块[1] 总 4 字节[1] 类型向量 1 个[2] 类型为函数[3][4] 0 个入参[4] 0 个出参[4] 函数区块[1][5] 共 2 字节[1]
00000010 01 00 08 01 00 0a 0a 01
备注 函数数量为1[2][5] 函数编号为 0[5] 开始区块[1][6] 共 1 字节[1] 调用函数编号为 0[6] 代码区块[1] 共 10 字节[1] 代码向量个数为1[7]
00000018 08 00 41 01 41 02 6a 0f
备注 代码共 8 字节[7] 本地变量数量为 0[7] i32.const 指令[8] 值为 1 i32.const 指令[8] 值为 2 i32.add 指令[8] return 指令[8]
00000020 0b
备注 end 指令[8]

表格参考:

  1. https://webassembly.github.io/spec/core/binary/modules.html#sections

    Each section consists of

    • a one-byte section id,
    • the u32 size of the contents, in bytes,
    • the actual contents, whose structure is depended on the section id.

    For most sections, the contents B encodes a vector.

    每个区块由【区块 ID (即类型)】+ 【区块大小】 + 【区块内容】组成,区块内容在大多数情况下为向量。0x01 为类型、0x03 为函数、0x08 为开始、0x0a 为代码。

  2. https://webassembly.github.io/spec/core/binary/conventions.html#binary-vec

    Vectors are encoded with their u32 length followed by the encoding of their element sequence.

    每个向量由【向量个数】+ 【向量】组成

  3. https://webassembly.github.io/spec/core/appendix/index-types.html

    类型为 0x60 为函数类型

  4. https://webassembly.github.io/spec/core/syntax/types.html#syntax-functype

    Function types classify the signature of functions, mapping a vector of parameters to a vector of results.

    函数类型,后续跟两个类型,分别为入参和出参。

  5. https://webassembly.github.io/spec/core/binary/modules.html#binary-funcsec

    It decodes into a vector of type indices that represent the type fields of the functions in the funcs component of a module.

    函数区块后面跟一个代码函数编码的向量。

  6. https://webassembly.github.io/spec/core/binary/modules.html#binary-startsec

    The start section has the id 8. It decodes into an optional start function that represents the start component of a module.

    开始区块,可以通过直接指定函数 id 编号来执行。

  7. https://webassembly.github.io/spec/core/binary/modules.html#code-section

    They represent the locals and body field of the functions in the funcs component of a module. codedesc = vec(code) code = size + func func = locals + expr

    由于没有本地变量,所以直接是函数个数说明,已经后续跟进的函数体的大小。

  8. https://webassembly.github.io/spec/core/appendix/index-instructions.html

    表达式指令集

Debug on Browser

Chrome 已原生支持 WASM 调试,当上述代码执行之后,可以看到 stack 的最终 value 为 3,符合预期。

image

常见问题

  • 执行 clang 的时候,报 no available targets are compatible
    • 参考 172, 需要安装最新的 llvm: brew install llvm,而非 mac 自带的 clang 工具。

参考

  • https://wasmbyexample.dev/examples/hello-world/hello-world.c.en-us.html
  • https://pengowray.github.io/wasm-ops/
  • https://depth-first.com/articles/2019/10/16/compiling-c-to-webassembly-and-running-it-without-emscripten/
  • https://developer.mozilla.org/en-US/docs/WebAssembly/Text_format_to_wasm
  • https://github.com/webassembly/wabt
  • https://webassembly.github.io/spec/core/binary/index.html#high-level-structure
  • http://troubles.md/wasm-is-not-a-stack-machine/