资讯专栏INFORMATION COLUMN

图说 WebAssembly(四):快速入门

BoYang / 2793人阅读

摘要:本文是图说系列文章的第四篇。它们表示一种可以在普遍流行机器上高效使用的指令集合。这是因为是一种称为堆栈机器。尽管是根据堆栈机器来设计的,但是这并不是它在真实物理机器上工作的方式。这些内容称为段。

本文是图说 WebAssembly 系列文章的第四篇。如果您还未阅读之前的文章,建议您从第一篇入手。

WebAssembly 是一种使得除 JavaScript 以外的编程语言也能运行在网页上的技术。
在过去,当我们需要通过编程来控制网页内容时,我们的选择只有 JavaScript 。

所以当大家都说 WebAssembly 运行速度很快时,其实它的比较对象就是指 JavaScript 。
不过这并不意味着你只能使用 JavaScript 和 WebAssembly 中的一种。
反而,更推荐的做法是同时使用它们。即便是你不写 WebAssembly ,你也是可以从它身上获得好处的。

WebAssembly 模块定义了可以被 JavaScript 调用的函数。
就像我们现在可以直接从 npm 下载 lodash 模块并调用其接口一样,未来我们也可以下载 WebAssembly 模块并使用它。

所以,今天我们来看看如何创建 WebAssembly 模块,以及如何使用 JavaScript 调用它。

角色

在上一篇文章中,我们介绍了编译器如何把高级语言编译为机器码。

在上图中,WebAssembly 对应哪个角色呢?

聪明的你可能已经想到,它只不过是另一种目标汇编语言而已。
从某种意义上来说,这种想法是对的,只不过图中的 x86、ARM 等其实对应的是一种特定的计算机架构。

对于开发者来说,他所开发的代码是希望能够运行在互联网上所有用户机器上的,但是他其实并不知道运行这些代码的机器属于哪种架构。

所以 WebAssembly 跟汇编相比还是有略微不同之处。
它面向的是一种概念上机器的机器语言,而不是一种真实存在的物理机器。

这也就导致了 WebAssembly 指令是一种虚拟指令
与 JavaScript 源码相比,虚拟指令跟机器码的映射来得更为直接。
它们表示一种可以在普遍流行机器上高效使用的指令集合。但同时它们也不会直接映射到特定的机器码。

浏览器会下载 WebAssembly,然后把它变成目标机器的汇编。

编译

目前对 WebAssembly 支持最多的编译器工具链称为 LLVM 。有很多不同的编译器前端和后端都在使用 LLVM 。

注意: 大多数的 WebAssembly 模块开发者都会使用 C 和 Rust 这样的语言,然后编译为 WebAssembly,但是也有其他方式创建 WebAssembly 模块。比如,有一个实验工具可以把 TypeScript 编译为 WebAssembly 模块,更有甚者,
可以直接手写 WebAssembly 。

这里,假如我们想把 C 编译为 WebAssembly 。
我们可以使用 C 语言编译器前端把 C 代码编译为 LLVM 中间代码。一旦变成 LLVM 的中间代码,LLVM 就可以理解并分析代码,然后做一些优化。

为了把 LLVM 中间代码变成 WebAssembly,我们还需要一个编译器后端。刚好,LLVM 项目中确实有一个正在开发编译器后端,未来它应该是大部分人的共同选择,而且应该很快就要完成了。不过,现在用它的话还是相当棘手。

不过不用灰心,还有另一个工具称为 Emscripten,目前用起来会更加简单点。
它拥有自己编译器后端,可以把中间代码编译为 asm.js ,进而转化为 WebAssembly 。
不过它也支持 LLVM,因此我们也可以在 Emscripten 和其他后端之间相互切换。

Emscripten 还包含了很多其他工具和库,允许开发者移植整个 C/C++ 代码,因此与其说它是编译器,其实它更像是软件开发套件(SDK)。

不管用什么工具链,最终的结果都是得到一个 .wasm 文件。后面我们会介绍 .wasm 文件的结构,不过首先让我们来看看如何在 JavaScript 中使用它。

加载

.wasm 文件就是 WebAssembly 模块,它可以直接使用 JavaScript 加载。
截止到目前,这种加载方式略微复杂。

function fetchAndInstantiate(url, importObject) {
    return fetch(url).then(res => res.arrayBuffer())
        .then(bytes => WebAssembly.instantiate(bytes, importObject))
        .then(results => results.instance);
}
想深入的话,可以参考这个MDN 文档

我们正在努力把这个过程变得更加简单。我们也希望能够把工具链变得更加友好,希望能够直接集成到诸如 webpack 或者 SystemJS 等打包器中。相信未来 WebAssembly 模块可以跟加载 JavaScript 模块一样简单好用。

不过,WebAssembly 模块和 JavaScript 模块之间有一个主要的不同之处。
当前,WebAssembly 模块中的函数只能使用数字作为参数或者返回值。

对于其他任何更复杂的数据类型,如字符串,我们必须直接操作 WebAssembly 模块的内存。

如果你大部分的时间都在使用 JavaScript,那么你可能对直接操作内容不太熟悉。
像 C、C++ 和 Rust 这些高性能的语言,它们都必须手动管理内存。
WebAssembly 模块的内存就模拟了这些语言的堆内存。

为了能够操作内存,我们需要使用 JavaScript 中的 ArrayBuffer
它是字节数组,所以它的索引当做内存地址来使用。

如果想要在 JavaScript 和 WebAssembly 之间传递字符串,那么必须先把字符串转为等效的字符码,然后写入 ArrayBuffer。由于数组索引是整数,所以索引可以传递给 WebAssembly 函数。这样,索引就变成了指向字符串首个字符的指针了。

不过大部分情况下,WebAssembly 模块开发者都会把模块做友好地封装。此时,模块的使用者可能就没必要知道其内部是如何管理内存的了。

如果你对内存管理感兴趣,可以查看 MDN 文档
结构

如果你编程使用的是高级语言然后编译为 WebAssembly,那其实你没必要了解 WebAssembly 模块的结构,不过它可以帮你理解基础信息。

下面是一个 C 函数,我们将把它编译为 WebAssembly 。

int add42(int num) {
    return num + 42;
}

你可以使用 WasmExplorer来编译这个函数。

打开编译好的 .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 的模样:

运行

你可能会对上图中的内容感到疑惑,下面我们把这些指令的作用标注出来。

你可能已经注意到 add 操作并没有说要相加的两个数从哪里来。这是因为 WebAssembly 是一种称为堆栈机器。这意味着,操作码在操作之前,它所需的操作数已经在堆栈的队列当中了。

add 这样的操作码本身就知道它需要多少个操作数。因为 add 需要两个操作数,所以它会从堆栈的顶部取出两个值来作为操作数。
这样种设计中,add 指令可以变得很短,只占用一字节,因为它并不需要指定源和目标寄存器地址。这样就减小了 .wasm 文件的大小,从而更利于网络传输。

尽管 WebAssembly 是根据堆栈机器来设计的,但是这并不是它在真实物理机器上工作的方式。
当浏览器把 WebAssembly 编译为机器码时,它仍然会用到寄存器。不过,由于 WebAssembly 代码并不指定寄存器,所以浏览器能够更自由的为其指定最高效的寄存器。

组成

除了 add42 函数本身,.wasm 也还包含了其他内容。这些内容称为(Section)。有些段是任何模块都必须有的,有些则是可选的。

必选的有:

类型:包含模块中函数和任何导入函数的函数签名。

函数:给模块中的每个函数提供索引。

代码:模块中每个函数的函数体。

可选的有:

导出:使得函数、内存、表格和全局变量对其他模块和 JS 可访问。这可以使得模块可以多带带编译,然后动态链接起来。

导入:指定从其他模块或者 JS 中导入的函数、内存、表格和全局变量等。

入口:模块加载时自动运行的函数。

全局:模块中的全局变量声明。

内存:定义模块使用的内存。

表格:用于映射不透明值,这些值不能在 WebAssembly 中表示或直接访问,例如 JS 的对象。

数据:用于初始化导入的或本地的内存

元素:用于初始化导入的或者本地的表格

更多的资料可参考 MDN 文档
结束

经过本文,相信你已经知道该如何使用 WebAssembly 模块了。下一篇文章我们将探索它为何如此快。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94766.html

相关文章

  • 图说 WebAssembly(一):序言

    摘要:性能简史在年,被创造出来时并不是冲着性能去的。而且在之后的十年发展中,它的性能一直是很低的。的引入成就了性能提升的一个转折点,其执行速度比以往快了之多。性能提升也使得在全新的问题上使用成为可能。现在,极可能是下一个性能转折点。 你可能已经听说 WebAssembly 代码跑起来非常快。但是你知道这是为什么吗?在本系列文章中,我们将探究其原因。 何为 WebAssembly WebAss...

    codergarden 评论0 收藏0
  • 图说 WebAssembly(六):现状与展望

    摘要:现状年月日,主流的四大浏览器达成了共识并宣布的最小可行产品已经完成。更快的函数调用当前,在中调用函数比想象的要慢。直接操作目前,没有任何方式能够操作。这就导致了部分应用可能会因此而推迟发布时间。结束现如今已经相当快速。 本文是图说 WebAssembly 系列文章的最后一篇。如果您还未阅读之前的文章,建议您从第一篇入手。 现状 2017 年 2 月 28 日,主流的四大浏览器达成了共识...

    clasnake 评论0 收藏0
  • 图说 WebAssembly(五):高性能原因

    摘要:本文是图说系列文章的第五篇。这样的话,使用的开发者也不需要做任何适配,但是它们却能获得更高性能。该图并不是用来准确的衡量其性能的。运行编写出高性能的代码是可能的。这种清理工作由引擎自动进行,称为垃圾回收。 本文是图说 WebAssembly 系列文章的第五篇。如果您还未阅读之前的文章,建议您从第一篇入手。 在上一篇文章中,我们说到了使用 WebAssembly 和 JavaScript...

    seal_de 评论0 收藏0
  • 图说 WebAssembly(二):JIT 编译器

    摘要:编译器优缺点与解释器相比,编译器有着相反的优缺点。它们为引擎新增了一个组件,称为监视器,或者。优化编译器会基于监视器记录的代码运行信息来作出一些判断。通常来说,优化编译器会使得代码跑的更快。而这正是优化编译器所做的优化之一。 本文是图说 WebAssembly 系列文章的第二篇,如果你还没阅读其它的,建议您从第一篇开始。 JavaScript 的运行,一开始是很慢的,但是后面会变得越来...

    LuDongWei 评论0 收藏0
  • 图说 WebAssembly(三):什么是汇编

    摘要:为了更好的理解,我们有必要去先理解什么是汇编,以及编译器是如何产生汇编的。什么是汇编现在,我们来看看外星人的大脑是如何工作的。这些注释就是汇编,也称为符号机器码。结束以上的内容就是什么是汇编以及它是如何从高级编程语言翻译过来的。 本文是图说 WebAssembly 系列文章的第三篇。如果您还未阅读之前的文章,建议您从第一篇入手。 为了更好的理解 WebAssembly ,我们有必要去先...

    刘福 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<