阅读 274

创建并使用 WebAssembly 模块

本文为翻译

原文标题:Creating and working with WebAssembly modules

原文作者:Lin Clark

原文地址:hacks.mozilla.org/2017/02/cre…

这是 WebAssembly 文章系列的第四部分。如果你还没有读过其他的,我建议你 从头开始

WebAssembly 是一种在 Web 页面中运行 JavaScript 外语言的方式。过去,如果需要让浏览器中运行的代码和 Web 页面中的不同部分进行交互,你只能选择使用 JavaScript。

所以,当人们说 “WebAssembly 速度很快” 时,是在和 JavaScript 做各方面的对比。但是这不意味着两者是非此即彼的 — 要么使用 WebAssembly,要么使用 JavaScript。

事实上,我们期望开发者能在同一个应用程序中同时使用 WebAssembly 和 JavaScript。即使你不亲自写 WebAssembly 代码,你也会从中获利。

WebAssembly 模块定义了暴露给 JavaScript 调用的 函数。因此,就像现在你可以从 npm 拉取 lodash,然后通过 API 调用这个库的函数一样,在将来你也能拉取 WebAssembly 模块。

那么,让我们看看如何创建 WebAssembly 模块,又如何从 JavaScript 中使用它。

WebAssembly 在流程中处于什么位置?

在关于汇编的那篇文章中,我谈论了编译器 如何将 高阶语言 编译成 机器码。

DraggedImage.6a97cf81c146400aad969f4b97962402 copy.png

在这张图片中,WebAssembly 应该插入在什么位置呢?

你可能会认为 WebAssembly 只是另外一种 目标汇编语言。这答对了一部分,因为除了 WebAssembly,图中的那些语言(x86, ARM)都对应了一种机器架构。

当通过 网络 来将 代码 发送到用户的机器上时,代码运行需要的目标架构信息是未知的。

所以 WebAssembly 和那些汇编语言有些许不同。它是一种机器语言,但对应的是 概念上的机器,不是真实存在的 或 物理存在的 机器。

因此,WebAssembly 的 指令 有时被称作 虚拟指令(virtual instructions)。相比于 JavaScript 源码,虚拟指令会更直接的映射到机器码。虚拟指令 表示了一类 在主流硬件中都能高效执行的 指令,而不是直接映射到 特定机器的 特定一段机器码。

浏览器下载 WebAssembly 代码后,可以轻轻松松的 从 WebAssembly 编译到 目标机器汇编码。

编译到 .wasm

目前来讲,对 WebAssembly 支持最好的编译器工具链是 LLVM。大量不同种类的 编译器前端编译器后端 都能被接入到 LLVM。

注意:大部分 WebAssembly 模块开发者 会使用 C 或者 Rust 这样的语言编码,然后编译成 WebAssembly,但是还是存在其他创建 WebAssembly 模块 的方式。例如,有一个实验性的工具,可以帮助你使用 TypeScript 创建 WebAssembly 模块,或者你也可以直接以 WebAssembly 的 文本表示 编码

假设我们要从 C 编译到 WebAssembly。我们可以使用 clang 的前端 来将 C 编译成 LLVM 的 IR(intermediate representation 中间表示)。一旦代码进入 LLVM 的IR 状态,LLVM 就能够解析代码,并执行一些优化。

为了将 LLVM 的 IR 编译为 WebAssembly,我们需要一个 编译器后端 。目前在 LLVM 项目中就有一个 编译器后端 在正在开发。这个 编译器后端 已经开发的差不多了,应该不久后就能完成。不过,目前你还没法使用。

现在,我们可以使用 Emscripten 作为替代。它有自己的 编译器后端 ,这个 编译器后端 可以帮助我们先编译到另一个目标(被称作 asm.js)并随后转换到 WebAssembly。Emscripten 在底层使用 LLVM,所以你可以随意切换 Emscripten 的两个 编译器后端

Emscripten 包含很多 附加工具 和 库,这使得 Emscripten 能够帮助哦移植整个 C/C++ 代码库。因此 Emscripten 比起 编译器 更像是一个 SDK 。例如,操作系统开发者 习惯于 拥有一个能读写的文件系统,而 Emscripten 就可以通过 IndexedDB 模拟一个文件系统。

无论你使用了什么工具链,最终结果都是生成一个 .wasm 文件。我会在下文介绍关于 .wasm 文件结构 的更多信息。在此之前,先让我们看看如何在 JS 中使用 .wasm 文件。

在 JavaScript 中加载 .wasm 模块

.wasm 文件 就是 WebAssembly 模块,它可以在 JavaScript 代码中加载。目前,加载的过程还有些繁琐。

function fetchAndInstantiate(url, importObject) {
  return fetch(url).then(response =>
    response.arrayBuffer()
  ).then(bytes =>
    WebAssembly.instantiate(bytes, importObject)
  ).then(results =>
    results.instance
  );
}
复制代码

你可以在我们的文档中深入了解更多内容。

我们正努力让这个过程变得更简单。我们期望通过像 webpack 或者 SystemJS 这样的 loader 来 对工具链进行改进。我们相信加载 WebAssembly 模块 有一天会像 加载 JavaScript 模块一样简单。

不过,WebAssembly 模块 和 JavaScript 模块 之间有一个重大的差异。目前,在 WebAssembly 中的函数只能以 数字类型(整数 或 浮点数) 作为 参数 或 返回值。

为了使用更复杂的数据类型,比如 字符串,你必须使用 WebAssembly 模块的内存。

如果你工作中基本只使用 JavaScript ,那你也许对 直接访问内存 并不熟悉。像 C、C++ 和 Rust 这些性能更好的语言,倾向手动管理内存。那些语言中 都有 “堆” 这个概念,WebAssembly 模块 会在内存模拟出 “堆”。

为了做到这一点,WebAssembly 模块会使用 JavaScript 中的 ArrayBuffer。这里的 “array buffer” 指的是 一个字节数组,数组的索引用来表示内存地址。

如果你想在 JavaScript 和 WebAssembly 之间传递字符串,你可以把这些字符转换为等效的字符编码。然后你需要把他们写入内存数组。因为索引是整数,所以索引可以被传递给 WebAssembly 函数。这样一来,字符串第一个字符的索引 就可以被用作 指针。

开发者 在编写 WebAssembly 模块 给 web 开发者使用时,很可能会在 模块 外面加一个 包装器。那么,你作为模块的使用者,就不需要了解内存管理的细节了。

你如果想了解更多,可以查看我们的 关于 WebAssembly 内存 的文档

.wasm 文件结构

如果使用高阶语言编写代码,然后编译成 WebAssembly,那你不需要知道 WebAssembly 模块 的构造模式。但了解其文件结构能帮助我们理解一些基础的东西。

如果你还没有读过关于编译的文章(本系列第三部分),那我建议你读一下。

这里有一个 C 函数,我们可以将它转化为 WebAssembly:

int add42(int num) {
  return num + 42;
}
复制代码

你可以尝试使用 WASM Explorer 来编译这个函数。

如果你打开 .wasm 文件(而且你的编辑器支持展示),你会看到如下内容。

00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
00 41 2A 6A 0B
复制代码

这是模块的 “二进制” 表示。我在二进制上加了引号,这是因为通常这种表示方法会使用 十六进制 作为记号,但这些记号转换成 二进制 或者人类可读的格式 很简单 。

例如,num + 42 的表示形式是这样的。

代码工作原理:堆栈结构机器(Stack machine)

下面是这些指令的作用,你可以看看。

你可能注意到了, add 操作 没有指明它的传参。这是因为 WebAssembly 是一个 堆栈结构机器。这意味着,在执行操作之前,操作所需的值都会在 堆栈 上排队。

add 这样的操作知道自己所需的参数数量。因为 add 需要两个参数,所以他会从 堆栈 顶部取下两个值。由于 add 指令不需要指定源寄存器 和 目标寄存器,所以 add 很短(一个单字节)。这样就降低了 .wasm 文件的大小,也意味着缩短了下载文件的时间。

尽管 WebAssembly 被制定为 堆栈结构机器,但在物理机上 WebAssembly 并不是以这种方式工作的。当浏览器将 WebAssembly 转化为 当前运行机器的机器码时,浏览器会使用寄存器。因为 WebAssembly 代码不特定寄存器,所以 WebAssembly 将这件事交给浏览器,浏览器可以灵活的选择当前机器最佳的寄存器分配方案。

模块的节段(sections)

除了 add42 函数本身,.wasm 文件内还有一些其他的部分。他们被称作节段。有些节段在任何模块中都是必须的,而有些是可选的。

必须的:

  1. Type :包含 定义在模块中的函数签名 和 任何引入(import)的函数。
  2. Function :给出定义在模块中的每个函数的索引。
  3. Code :模块中每个函数真实的函数体。

可选的:

  1. Export : 使 函数、内存、表 和 全局变量 暴露给 其他 WebAssembly 模块 或 JavaScript。这让单独的模块可以被动态链接在一起。这就是 WebAssembly 版本的 .dll。
  2. Import : 指定从其他 WebAssembly 模块 或 JavaScript 中引入的 函数、内存、表 和 全局变量。
  3. Start : 指定一个函数,当 WebAssembly 模块被加载时,函数被自动运行(基本类似于主函数)。
  4. Global : 为模块声明全局变量。
  5. Memory : 定义模块使用的内存。
  6. Table : 提供 映射到 WebAssembly 模块外部值 的能力,例如可以映射为 JavaScript 对象。在间接函数调用时,这个能力非常有用。
  7. Data:初始化 被导入的内存 或 本地内存。
  8. Element :初始化 被导入的表 或 本地表。

有关节段的更多信息,这里有一篇文章深入解释了这些节段如何工作

下一步

现在你已经了解了如何使用 WebAssembly 模块,让我们看看为什么 WebAssembly 这么快。

文章分类
前端
文章标签