资讯专栏INFORMATION COLUMN

WebAssembly 系列(四)WebAssembly 工作原理

stormzhang / 3203人阅读

摘要:但是它们其实并不是二选一的关系并不是只能用或者。正因为如此,指令有时也被称为虚拟指令。这是因为是采用基于栈的虚拟机的机制。声明模块的全局变量。。下文预告现在你已经了解了模块的工作原理,下面将会介绍为什么运行的更快。

作者:Lin Clark

编译:胡子大哈

翻译原文:http://huziketang.com/blog/posts/detail?postId=58c77641a6d8a07e449fdd24

英文原文:Creating and working with WebAssembly modules

转载请注明出处,保留原文链接以及作者信息

本文是关于 WebAssembly 系列的第四篇文章(本系列共六篇文章)。如果你没有读先前文章的话,建议先读这里。如果对 WebAssembly 没概念,建议先读这里(中文文章)。

WebAssembly 是除了 JavaScript 以外,另一种可以在网页中运行的编程语言。过去如果你想在浏览器中运行代码来对网页中各种元素进行控制,只有 JavaScript 这一种选择。

所以当人们谈论 WebAssembly 的时候,往往会拿 JavaScript 来进行比较。但是它们其实并不是“二选一”的关系——并不是只能用 WebAssembly 或者 JavaScript。

实际上,我们鼓励开发者将这两种语言一起使用,即使你不亲自实现 WebAssembly 模块,你也可以学习它现有的模块,并它的优势来实现你的功能。

WebAssembly 模块定义的一些功能可以通过 JavaScript 来调用。所以就像你通过 npm 下载 lodash 模块并通过 API 使用它一样,未来你也可以下载 WebAssembly 模块并且使用其提供的功能。

那么就让我们来看一下如何开发 WebAssembly 模块,以及如何通过 JavaScript 使用他们。

WebAssembly 处于哪个环节?

在上一篇关于汇编的文章中,我介绍了编译器是如何从高级语言翻译到机器码的。

那么在上图中,WebAssembly 在什么位置呢?实际上,你可以把它看成另一种“目标汇编语言”。

每一种目标汇编语言(x86、ARM)都依赖于特定的机器结构。当你想要把你的代码放到用户的机器上执行的时候,你并不知道目标机器结构是什么样的。

而 WebAssembly 与其他的汇编语言不一样,它不依赖于具体的物理机器。可以抽象地理解成它是概念机器的机器语言,而不是实际的物理机器的机器语言。

正因为如此,WebAssembly 指令有时也被称为虚拟指令。它比 JavaScript 代码更直接地映射到机器码,它也代表了“如何能在通用的硬件上更有效地执行代码”的一种理念。所以它并不直接映射成特定硬件的机器码。

浏览器把 WebAssembly 下载下来,然后先经过 WebAssembly 模块,再到目标机器的汇编代码。

编译到 .wasm 文件

目前对于 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 模块到 JavaScript

.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 的内存操作。

.wasm 文件结构

如果你是写高级语言的开发者,并且通过编译器编译成 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 代码并不指定用哪些寄存器,这样做的好处是给浏览器最大的自由度,让其自己来进行寄存器的最佳分配。

WebAssembly 模块的组成部分

除了上面介绍的,.wasm 文件还有其他部分,通常把它们叫做部件。一些部件对于模块来讲是必须的,一些是可选的。

必须部分

Type。在模块中定义的函数的函数声明和所有引入函数的函数声明。

Function。给出模块中每个函数一个索引。

Code。模块中每个函数的实际函数体。

可选部分

Export。使函数、内存、表单(table)、全局变量等对其他 WebAssembly 或 JavaScript 可见,允许动态链接一些分开编译的组件,即 .dll 的WebAssembly 版本。

Import。允许从其他 WebAssembly 或者 JavaScript 中引入指定的函数、内存、表单或者全局变量。

Start。当 WebAssembly 模块加载进来的时候,可以自动运行的函数(类似于 main 函数)。

Global。声明模块的全局变量。

Memory。定义模块用到的内存。

Table。使得可以映射到 WebAssembly 模块以外的值,如映射到 JavaScript 对象中。这在间接函数调用时很有用。

Data。初始化内存。

Element。初始化表单(table)。

如果想要了解更多的部件,可以在“如何使用部件”中深入了解。

下文预告

现在你已经了解了 WebAssembly 模块的工作原理,下面将会介绍为什么 WebAssembly 运行的更快。

我最近正在写一本《React.js 小书》,对 React.js 感兴趣的童鞋,欢迎指点。

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

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

相关文章

  • WebAssembly 系列(一)生动形象地介绍 WebAssembly

    摘要:但是为什么执行的更快呢在这个系列文章中,我会为你解释这一点。所以当人们说更快的时候,一般来讲是与相比而言的。被人们广为传播的性能大战在年打响。性能的提升使得的应用范围得到很大的扩展。现在通过,我们很有可能正处于第二个拐点。 作者:Lin Clark 编译:胡子大哈 翻译原文:http://huziketang.com/blog/posts/detail?postId=58ce8036...

    wangbjun 评论0 收藏0
  • WebAssembly 为什么比 asm.js 快?

    摘要:并且于年月日,四个主要的浏览器一致同意宣布的版本已经完成,即将推出一个浏览器可以搭载的稳定版本。因此本文着重介绍为什么比更快。本文主要表达的是为什么应该是更快的。则不同,它是由几大主要的浏览器厂商共同设计的。 作者:Alon Zakai 编译:胡子大哈 翻译原文:http://huziketang.com/blog/posts/detail?postId=58ce80d2a6d8a0...

    Binguner 评论0 收藏0
  • JavaScript 工作原理之六-WebAssembly 对比 JavaScript 及其使用场景

    摘要:现在,我们将会剖析的工作原理,而最重要的是它和在性能方面的比对加载时间,执行速度,垃圾回收,内存使用,平台访问,调试,多线程以及可移植性。目前,是专门围绕和的使用场景设计的。目前不支持多线程。 原文请查阅这里,略有改动,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland。 本系列持续更新中,Github 地址请查阅这里。 这是 JavaScript 工作原理的第六章...

    jay_tian 评论0 收藏0
  • 前端每周清单半年盘点之 WebAssembly

    摘要:前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。利用降低三倍加载速度自推出之后,很多开发者都开始尝试在小型项目中实践,不过尚缺大型真实案例比较。 前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为新闻热点、开发教程、工程实践、深度阅读、开源项目、巅峰人生等栏目...

    Alan 评论0 收藏0

发表评论

0条评论

stormzhang

|高级讲师

TA的文章

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