本文是图说 WebAssembly 系列文章的第四篇。若是您还未阅读以前的文章,建议您从第一篇入手。前端
WebAssembly 是一种使得除 JavaScript 之外的编程语言也能运行在网页上的技术。
在过去,当咱们须要经过编程来控制网页内容时,咱们的选择只有 JavaScript 。webpack
因此当你们都说 WebAssembly 运行速度很快时,其实它的比较对象就是指 JavaScript 。
不过这并不意味着你只能使用 JavaScript 和 WebAssembly 中的一种。
反而,更推荐的作法是同时使用它们。即使是你不写 WebAssembly ,你也是能够从它身上得到好处的。git
WebAssembly 模块定义了能够被 JavaScript 调用的函数。
就像咱们如今能够直接从 npm 下载 lodash 模块并调用其接口同样,将来咱们也能够下载 WebAssembly 模块并使用它。github
因此,今天咱们来看看如何建立 WebAssembly 模块,以及如何使用 JavaScript 调用它。web
在上一篇文章中,咱们介绍了编译器如何把高级语言编译为机器码。npm
在上图中,WebAssembly 对应哪一个角色呢?编程
聪明的你可能已经想到,它只不过是另外一种目标汇编语言而已。
从某种意义上来讲,这种想法是对的,只不过图中的 x8六、ARM 等其实对应的是一种特定的计算机架构。segmentfault
对于开发者来讲,他所开发的代码是但愿可以运行在互联网上全部用户机器上的,可是他其实并不知道运行这些代码的机器属于哪一种架构。后端
因此 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)。有些段是任何模块都必须有的,有些则是可选的。
必选的有:
可选的有:
更多的资料可参考 MDN 文档
通过本文,相信你已经知道该如何使用 WebAssembly 模块了。下一篇文章咱们将探索它为什么如此快。