建立和使用 WebAssembly 组件

这是 WebAssembly 系列文章的第四部分。若是你还没阅读过前面的文章,咱们建议你从头开始javascript

WebAssembly 是一种不一样于 JavaScript 的在 web 页面上运行程序语言的方式。之前当你想在浏览器上运行代码来实现 web 页面不一样部分的交互时,你惟一的选择就是 JavaScript。前端

所以当人们谈论 WebAssembly 运行迅速时,合理的比较对象就是 JavaScript。但这并不意味着你必须在 WebAssembly 和 JavaScript 两者中选择一个使用。java

事实上咱们但愿开发者在同一应用中同时使用 WebAssembly 和 JavaScript。即便你不亲自写 WebAssembly 代码,你也可使用它。react

WebAssembly 组件定义的函数能够在 JavaScript 中使用。所以,就像如今你能够从 npm 上下载一个 lodash 这样的组件而且根据它的 API 调用方法同样,在将来你一样能够下载 WebAssembly 组件。android

那么让咱们看看如何建立 WebAssembly 组件,以及如何在 JavaScript 中使用这些组件吧。webpack

WebAssembly 处于哪一个环节?

在上一篇关于汇编的文章里,我谈到过编译器怎么提取高级程序语言而且把它们翻译成机器码。ios

Diagram showing an intermediate representation between high level languages and assembly languages, with arrows going from high level programming languages to intermediate representation, and then from intermediate representation to assembly language

WebAssembly 对应这张图片的哪一个部分?git

你可能认为它只不过是又一个目标汇编语言。某种程度上是对的,不一样之处在于那些语言(x86,ARM)中每一个都对应一个特定的机器架构。github

当你经过 web 向用户的机器上发送要执行的代码时,你并不知道你的代码将要在哪一种目标架构上运行。web

因此 WebAssembly 和其余的汇编有些细微的差异。它是概念机的机器语言,而非真实的物理机。

正因如此,WebAssembly 指令有时也被称为虚拟指令。它们比 JavaScript 源码有更直接的机器码映射。它们表明一类能够在常见的流行硬件上高效执行的指令集合。可是它们并不直接映射某一具体硬件的特定机器码。

Same diagram as above with WebAssembly inserted between the intermediate representation and assembly

浏览器下载 WebAssembly 后,它就能从 WebAssembly 转成目标机器的汇编码。

编译成 .wasm

LLVM 是当前对 WebAssembly 支持最好的编译工具链。不少先后端编译工具均可以嵌入 LLVM 中。

注:大部分 WebAssembly 组件开发者用 C 和 Rust 这样的语言编写代码,而后编译成 WebAssembly,但仍有其余的方法来建立 WebAssembly 组件。好比,有一个实验性的工具帮你使用 TypeScript 构建 WebAssembly 组件,或者你能够直接在 WebAssembly 的文本表示上编码

好比说咱们想把 C 编译成 WebAssembly。咱们可使用 clang 编译器前端把 C 编译成 LLVM 中介码。一旦它处于 LLVM 的中间层,LLVM 编译它,LLVM 就能够展示一些性能优化。

要把 LLVM IR(中介码)编译成 WebAssembly,咱们须要一个后端支持。在 LLVM 项目中有一个这类后端正在开发中。这个后端项目已经接近完成而且应该很快就会定稿。然而,如今使用它还会有很多问题。

目前有一个稍微容易使用的工具叫 Emscripten。他有本身的后端,能够经过编译成其余对象(称为 asm.js)而后再转换成 WebAssembly 的方式来产生 WebAssembly。好像它底层仍旧使用 LLVM,所以你能够在 Emscripten 中切换这两种后端。

Diagram of the compiler toolchain

Emscripten 包含了许多附加工具和库来支持移植整个 C/C++ 代码库,所以它更像一个 SDK 而非编译器。举个例子,系统开发人员习惯于有一个文件系统用来读写,因此 Emscripten 可使用 IndexedDB 模拟一个文件系统。

忽略你已经使用的工具链,最后获得的结果就是一个后缀名为 .wasm 的文件。下面我将着重解释 .wasm 文件的结构。首先,咱们先看看怎样在JS中使用 .wasm 文件。

在 JavaScript 中载入一个 .wasm 组件

这个 .wasm 文件是一个 WebAssembly 组件,它能够在 JavaScript 中载入。在此情景下,载入过程稍微有些复杂。

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

你能够在咱们的文档中深刻了解这部份内容。

咱们致力于让这个过程变得更容易。咱们指望改进工具链,整合已存在的像 webpack 这样的模块打包工具以及相似 SystemJS 的动态加载器。咱们相信载入 WebAssembly 组件能够像载入 JavaScript 组件同样简单。

不过,WebAssembly 组件和 JS 组件有一个显著的区别。目前,WebAssembly 函数只能使用数字(整型或浮点型数字)做为参数和返回值。

Diagram showing a JS function calling a C function and passing in an integer, which returns an integer in response

对于更加复杂的数据类型,如字符串,你必须使用 WebAssembly 组件存储器。

像 C,C++,和 Rust 这些更高性能的语言倾向于手动管理内存。若是你大部分时间都在使用 JavaScript,也许对直接访问存储器的操做不熟悉。WebAssembly 组件存储器模拟了你在这些语言中会看到的堆。

为了实现这个功能,它使用了 JavaScript 中的类型化数组(ArrayBuffer)。类型化数组是存放字节的数组。数组的索引就是对应的存储器地址。

若是想要在 JavaScript 和 WebAssembly 中传递字符串,你须要把这些字符转换成他们的字符码常量。而后把这些写入存储器阵列。既然索引是整数,那么单个索引值就能够传入 WebAssembly 函数中。这样字符串中第一个字符的索引就能够被当成一个指针使用。

Diagram showing a JS function calling a C function with an integer that represents a pointer into memory, and then the C function writing into memory

几乎全部想要开发供 web 开发者使用的 WebAssembly 组件的开发者,都会为组件建立一个包装器。这样以来,你做为组件的消费者并不须要了解内存管理。

若是想了解更多的话,查看咱们关于使用 WebAssembly 内存的文档。

.wasm 文件结构

若是你使用高级语言来编写代码而后把它编译成 WebAssembly,你没必要知道 WebAssembly 组件的结构。可是它能够帮助你理解其基本原理。

若是你以前没有了解这些基本原理,咱们建议你先阅读 汇编文章 (part 3 of the series)。

下面是一个 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 的几种表现形式。

Table showing hexadecimal representation of 3 instructions (20 00 41 2A 6A), their binary representation, and then the text representation (get_local 0, i32.const 42, i32.add)

代码如何运行:堆栈机

若是你想知道的话,下图是执行的一些指令说明。

Diagram showing that get_local 0 gets value of first param and pushes it on the stack, i32.const 42 pushes a constant value on the stack, and i32.add adds the top two values from the stack and pushes the result

你可能注意到了 add 操做并无说明他的值应该从哪里来。这是由于 WebAssembly 是堆栈机的一个范例。这意味着一个操做所需的全部值在操做执行以前都在栈中排队。

例如 add 这类的操做指导它们须要多少值。若是 add 须要两个值,它将从栈顶取出两个值。这意味着 add 指令能够很短(单个字节),由于指令不须要指定源或者目的寄存器。这减小了 .wasm 文件的大小,也意味着下载的耗时更短。

即便 WebAssembly 就堆栈机而言是特定的,但那不是其在物理机上的工做方式。当浏览器把 WebAssembly 转化成其运行机器上对应的机器码时,将会用到寄存器。由于 WebAssembly 代码不指定寄存器,因此浏览器在当前机器上能更灵活的去使用最佳寄存器分配。

组件的 sections

除了 add42 函数自身,.wasm 文件还有其余部分。那就是 sections。一些 sections 对任何组件都是必需的,而有一些是可选的。

必选项:

  1. 类型(Type)。包括在该组件中定义的函数签名以及任何引入的函数。
  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)。初始化导入或本地的表。

更多关于 sections 的阐释,这有一篇深度好文解释这些 sections 如何运行

接下来

如今你知道怎样使用 WebAssembly 组件了,让咱们看看为何 WebAssembly 这么快

『系列文章之最终章』

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

相关文章
相关标签/搜索