做者:Lin Clark
编译:胡子大哈 javascript
翻译原文:huziketang.com/blog/posts/…
英文原文:Creating and working with WebAssembly modules前端
转载请注明出处,保留原文连接以及做者信息java
本文是关于 WebAssembly 系列的第四篇文章(本系列共六篇文章)。若是你没有读先前文章的话,建议先读这里。若是对 WebAssembly 没概念,建议先读这里(中文文章)。react
WebAssembly 是除了 JavaScript 之外,另外一种能够在网页中运行的编程语言。过去若是你想在浏览器中运行代码来对网页中各类元素进行控制,只有 JavaScript 这一种选择。webpack
因此当人们谈论 WebAssembly 的时候,每每会拿 JavaScript 来进行比较。可是它们其实并非“二选一”的关系——并非只能用 WebAssembly 或者 JavaScript。git
实际上,咱们鼓励开发者将这两种语言一块儿使用,即便你不亲自实现 WebAssembly 模块,你也能够学习它现有的模块,并它的优点来实现你的功能。github
WebAssembly 模块定义的一些功能能够经过 JavaScript 来调用。因此就像你经过 npm 下载 lodash 模块并经过 API 使用它同样,将来你也能够下载 WebAssembly 模块而且使用其提供的功能。web
那么就让咱们来看一下如何开发 WebAssembly 模块,以及如何经过 JavaScript 使用他们。npm
在上一篇关于汇编的文章中,我介绍了编译器是如何从高级语言翻译到机器码的。编程
那么在上图中,WebAssembly 在什么位置呢?实际上,你能够把它当作另外一种“目标汇编语言”。
每一种目标汇编语言(x8六、ARM)都依赖于特定的机器结构。当你想要把你的代码放到用户的机器上执行的时候,你并不知道目标机器结构是什么样的。
而 WebAssembly 与其余的汇编语言不同,它不依赖于具体的物理机器。能够抽象地理解成它是概念机器的机器语言,而不是实际的物理机器的机器语言。
正由于如此,WebAssembly 指令有时也被称为虚拟指令。它比 JavaScript 代码更直接地映射到机器码,它也表明了“如何能在通用的硬件上更有效地执行代码”的一种理念。因此它并不直接映射成特定硬件的机器码。
浏览器把 WebAssembly 下载下来,而后先通过 WebAssembly 模块,再到目标机器的汇编代码。
目前对于 WebAssembly 支持状况最好的编译器工具链是 LLVM。有不少不一样的前端和后端插件能够用在 LLVM 上。
提示:不少 WebAssembly 开发者用 C 语言或者 Rust 开发,再编译成 WebAssembly。其实还有其余的方式来开发 WebAssembly 模块。例如利用 TypeScript 开发 WebAssembly 模块,或者直接用 WebAssembly 文本也能够。
假设想从 C 语言到 WebAssembly,咱们就须要 clang 前端来把 C 代码变成 LLVM 中间代码。当变换成了 LLVM IR 时,说明 LLVM 已经理解了代码,它会对代码自动地作一些优化。
为了从 LLVM IR 生成 WebAssembly,还须要后端编译器。在 LLVM 的工程中有正在开发中的后端,并且应该很快就开发完成了,如今这个时间节点,暂时还看不到它是如何起做用的。
还有一个易用的工具,叫作 Emscripten。它经过本身的后端先把代码转换成本身的中间代码(叫作 asm.js),而后再转化成 WebAssembly。实际上它背后也是使用的 LLVM。
Emscripten 还包含了许多额外的工具和库来包容整个 C/C++ 代码库,因此它更像是一个软件开发者工具包(SDK)而不是编译器。例如系统开发者须要文件系统以对文件进行读写,Emscripten 就有一个 IndexedDB 来模拟文件系统。
不考虑太多的这些工具链,只要知道最终生成了 .wasm 文件就能够了。后面我会介绍 .wasm 文件的结构,在这以前先一块儿了解一下在 JS 中如何使用它。
.wasm 文件是 WebAssembly 模块,它能够加载到 JavaScript 中使用,现阶段加载的过程稍微有点复杂。
function fetchAndInstantiate(url, importObject) {
return fetch(url).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results =>
results.instance
);
}复制代码
若是想深刻了解,能够在 MDN 文档中了解更多。
咱们一直在致力于把这一过程变得简单,对工具链进行优化。但愿可以把它整合到现有的模块打包工具中,好比 webpack 中,或者整合到加载器中,好比 SystemJS 中。咱们相信加载 WebAssembly 模块也能够像加载 JavaScript 同样简单。
这里介绍 WebAssembly 模块和 JavaScript 模块的主要区别。当前的 WebAssembly 只能使用数字(整型或者浮点型)做为参数或者返回值。
对于任何其余的复杂类型,好比 string,就必须得用 WebAssembly 模块的内存操做了。若是是常用 JavaScript,对直接操做内存不是很熟悉的话,能够回想一下 C、C++ 和 Rust 这些语言,它们都是手动操做内存。WebAssembly 的内存操做和这些语言的内存操做很像。
为了实现这个功能,它使用了 JavaScript 中称为 ArrayBuffer 的数据结构。ArrayBuffer 是一个字节数组,因此它的索引(index)就至关于内存地址了。
若是你想在 JavaScript 和 WebAssembly 之间传递字符串,能够利用 ArrayBuffer 将其写入内存中,这时候 ArrayBuffer 的索引就是整型了,能够把它传递给 WebAssembly 函数。此时,第一个字符的索引就能够当作指针来使用。
这就好像一个 web 开发者在开发 WebAssembly 模块时,把这个模块包装了一层外衣。这样其余使用者在使用这个模块的时候,就不用关心内存管理的细节。
若是你想了解更多的内存管理,看一下咱们写的 WebAssembly 的内存操做。
若是你是写高级语言的开发者,而且经过编译器编译成 WebAssembly,那你不用关心 WebAssembly 模块的结构。可是了解它的结构有助于你理解一些基本问题。
若是你对编译器还不了解,建议先读一下“WebAssembly 系列(三)编译器如何生成汇编”这篇文章。
这段代码是即将生成 WebAssembly 的 C 代码:
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 的各类表示方法。
若是你对具体的操做过程很好奇,那么这幅图能够告诉你指令都作了什么。
从图中咱们能够注意到 加
操做并无指定哪两个数字进行加。这是由于 WebAssembly 是采用“基于栈的虚拟机”的机制。即一个操做符所须要的全部值,在操做进行以前都已经存放在堆栈中。
全部的操做符,好比加法,都知道本身须要多少个值。加
须要两个值,因此它从堆栈顶部取两个值就能够了。那么加
指令就能够变的更短(单字节),由于指令不须要指定源寄存器和目的寄存器。这也使得 .wasm 文件变得更小,进而使得加载 .wasm 文件更快。
尽管 WebAssembly 使用基于栈的虚拟机,可是并非说在实际的物理机器上它就是这么生效的。当浏览器翻译 WebAssembly 到机器码时,浏览器会使用寄存器,而 WebAssembly 代码并不指定用哪些寄存器,这样作的好处是给浏览器最大的自由度,让其本身来进行寄存器的最佳分配。
除了上面介绍的,.wasm 文件还有其余部分,一般把它们叫作部件。一些部件对于模块来说是必须的,一些是可选的。
必须部分:
可选部分:
若是想要了解更多的部件,能够在“如何使用部件”中深刻了解。
如今你已经了解了 WebAssembly 模块的工做原理,下面将会介绍为何 WebAssembly 运行的更快。
我最近正在写一本《React.js 小书》,对 React.js 感兴趣的童鞋,欢迎指点。