阅读 400

初识 WASM

Wasm 是什么?

Wasm(WebAssembly) 是一种底层的汇编语言,能够在所有当代桌面浏览器及很多移动浏览器中以接近本地的速度运行。

Wasm 解决了什么问题?

  • 解决 JavaScript 的性能问题,比如各种图形处理
  • 在浏览器中复用其他语言的代码成为可能,比如将桌面端的应用移植到 Web 中
  • 在浏览器中使用其他语言更成熟更快的库,比如音视频处理库 ffmpeg、各种 AI 库

Wasm 的工作原理

由其他语言编译到 Wasm 文件分为两个步骤

  • 前端

    编写的代码编译为一种中间表示 IR(intermediaterepresentation)

    epub_37730875_6-8344664.jpeg

  • 后端

    通过一种专用的编译器将 IR 编译为二进制码写入 Wasm 文件

epub_37730875_7-8344689.jpeg

浏览器将 Wasm 文件加载后,还要进行一次编译,将二进制码编译为机器码

epub_37730875_7.jpeg

这里有个关注点,为什么将二进制码编译为机器码不能放到上一步中,打包到 Wasm 文件中而省去下载后编译的时间呢?

因为不同设备的处理器不同,这可能会使 Wasm 文件的适配版本非常繁复,由浏览器编译字节码到运行设备的机器码,增加了 Wasm 文件的通用性。

Wasm 的内存

Wasm 共享 JavaScript VM,而 JavaScript VM 是虚拟化的,Wasm 对内存没有直接的访问权限,需要在初始化模块传递一个 ArrayBuffer 作为其线性内存,如果没有传递则自动创建。

image-20210808205510435-8427312.png

ArrayBuffer 既是 JavaScript 中的对象,又是 Wasm 中的内存,有两个好处

  • 使内存管理更加安全

    每当 WebAssembly 中有操作内存时,引擎会进行数组限制检查,以确保该地址位于 WebAssembly 实例的内存中。如果代码尝试访问超出范围的地址,引擎将抛出异常。

image-20210808205857217-8427539.png

  • 方便 JavaScript 与 Wasm 交流数据

一个 Wasm 的简单例子

由一个实例理解 Wasm 的过程,目标是在 JS 中调用 C 代码的 add 函数

  • 先安装 emscripten (C 和 C++ 专用编译到 Wasm的工具),创建一个文件夹后,在命令行依次输入以下命令

    git clone https://github.com/emscripten-core/emsdk.git 
    cd emsdk
    git pull
    ./emsdk install latest
    ./emsdk activate latest
    source ./emsdk_env.sh // 设置环境变量
    复制代码

    最后输入 emcc,提示不是 comment not found 就说明安装成功了

    这是 Mac 的命令,在 Windows上用 emsdk 代替 ./emsdk,emsdk_env.bat 代替 source ./emsdk_env.sh 官方教程

    这里需要注意一点,每个项目都应该安装一次 emsdk,它不是全局的工具,每次打开命令行都需要输入一次 source ./emsdk_env.sh,前面的不用,它仅设置当前终端能识别到 emcc。

  • 创建 add.c 文件,写入代码

    int add(int a, int b)
    {
      return a + b;
    }
    复制代码

    简简单单~

  • 使用刚刚的那个终端,编译 c 文件到 Wasm

    emcc add.c -s SIDE_MODULE=1 -o add.wasm
    复制代码

    你就可以看到目录下面多了一个 add.wasm 文件了,说明一下参数的大概意思

    • -s 必填,表明编译到 Wasm,否则编译到 Asm.js(Wasm 的前身), 最初 emscripten 是用于编译到 Asm.js 的。

    • SIDE_MODULE=1,表明编译为副模块。有副模块就有主模块,简单理解副模块会去除 C 标准库函数,因为副模块会在运行时被链接到一个主模块,而主模块有C标准库函数。

      我们这里没有使用到标准库中的函数,所以可以直接编译为副模块,SIDE_MODULE的值可选 1 或者 2,前者会自动导出代码里所有的函数,而后者需要手动声明。

    • -o add.wasm 导出选项,导出的文件可选 .html.js.wasm,区别在于前面两者会帮你把胶水代码写好 ,而 .wasm 则需要在 JS 自己编写胶水代码了,但是前面两者代码冗余,比如编译为 JS 文件时,JS 文件会包含两千多行代码,不过这是学习 Wasm 的现成实例。

  • 编写 JS 代码,根据上面的原理,之后的流程:

    下载 Wasm 文件 -> 编译 Wasm 为机器码 -> 实例化 -> 调用函数

    在 add.wasm 同目录下创建 index.html ,在 script 标签中写入加载代码

    async function load() {
            const buffer = await fetch('./add.wasm')
            const arrayBuffer = await buffer.arrayBuffer()
            const module = await WebAssembly.compile(arrayBuffer) // await WebAssembly.compileStreaming(fetch('./add.wasm'))
            const importObj = {
              env: {
                memory: new WebAssembly.Memory({ initial: 10, maximum: 256 }),
                __memory_base: 0,
                __table_base: 0
              }
            }
            const instance = await WebAssembly.instantiate(module, importObj)
            console.log(instance.exports.add(60, 11))
    }
    window.onload = load
    复制代码

    vscode 可以安装 Live Server 插件,然后右键 index.html 点击 Open With Live Server 简单打开一个本地服务,因为 fetch 不支持 file 协议,所以直接打开 index.html 会导致 fetch 报错。

    代码的过程很清晰,有几个点

    • Wasm 文件目前只能通过请求的方式引入,<script type="module"><script> 的方式还暂不支持

    • 请求 Wasm 文件返回的是流,可以将前三步合并

      const module = await WebAssembly.compileStreaming(fetch('./add.wasm')),这种方式采用流编译的方式,边下载边编译,所以速度会更快,不过前提是浏览器支持 compileStreaming且 Wasm 文件的响应头的 Content-Type 为 application/wasm,不过 live server 无法配置返回头

      关于流,可以看这篇文章 mp.weixin.qq.com/s/7x9cifE2A…

    • module 可以被 结构化克隆,意味着它可以通过 worker 的 postMessage 传递或者储存到 IndexDB 之中

    • importObj 的配置后续文章再分析

至此,你可以看到控制台打印出 71。

参考

文章分类
前端