原文链接: https://web.dev/what-is-webassembly/
原文作者:Thomas Steiner
自从网络不仅成为文档平台,也成为应用程序平台以来,那些最先进的 Web 应用已经将浏览器推向了极限。在许多高级语言中,我们都能看到通过低级语言接口来让程序更“贴近硬件”的方法,例如 Java 的 Java Native Interface。对于 JavaScript 来说,这个低级语言就是 WebAssembly。在这篇文章中,你会了解到什么是汇编语言,为什么汇编语言在 Web 平台中非常有用,然后了解 WebAssembly 是如何在 asm.js 这个临时解决方案的基础上产生发展的。
汇编语言
你用汇编语言写过程序吗?在计算机编程中,汇编语言——通常被简称为 Assembly 或者缩写为 ASM——指代所有低级语言,这类语言的语句和体系架构的机器码有着很强的对应关系。
以 英特尔 64 与 IA-32 架构为例 (PDF) 为例,其 MUL
指令(也就是乘法 multiplication)会对第一个操作数(目标操作数)和第二个操作数(源操作数)进行无符号惩罚,然后将结果存储在目标操作数中。简单来说,目标操作数是一个位于寄存器 AX
中的隐含操作数,而源操作数则位于类似 CX
这样的通用寄存器中。最后结果会被存储到 AX
寄存器中。下面是一个 x86 代码的例子:
|
|
比较一下,如果让你用 JavaScript 代码来实现 5 * 10 这个功能,大概会是这样的:
|
|
相比于使用高级且可读性强的语言,使用汇编语言来实现的好处是它足够低级和接近硬件,因此执行效率非常高。在上面的例子中两者的效率区别不会很明显,但对于更复杂的操作来说,就会有很大的不同了。
从名字就可以看出来,x86 代码依赖于 x86 架构。是否有一种方法让我们可以编写不依赖于特定架构的汇编语言,同时又仍然保留汇编语言的性能优势?
asm.js
编写不依赖于特定架构的汇编语言的第一步尝试是 asm.js,它是一个 JavaScript 的严格子集,可作为编译器的低级且高效的目标语言。这个子语言有效地提供了一个供 C 或 C++ 等内存不安全语言使用的沙盒虚拟机。结合静态和动态的验证,JavaScript 可以采用 AOT(ahead-of-time)优化编译策略来编译出有效的 asm.js 代码。使用具有手动内存管理功能的静态类型语言(如 C)编写的代码由“源码到源码”编译器(如早期的 Emscripten,基于 LLVM)进行翻译。
通过将语言功能限制在适合 AOT 的范围内,程序运行的性能得到了提高。火狐 22 是第一个支持 asm.js 的浏览器,以 OdinMonkey 的名称发布。Chrome 在第 61 版中增加了对 asm.js 的支持。虽然 asm.js 仍能在浏览器中运行,但它已被 WebAssembly 所取代。目前使用 asm.js 的原因是,它可以作为不支持 WebAssembly 的浏览器的替代方案。
WebAssembly
WebAssembly 是一种低级汇编语言,采用紧凑的二进制格式,其运行性能接近原生语言。它可以作为 C/C++ 和 Rust 等语言的编译目标,使其能在网络上运行。对 Java、Kotlin 和 Dart 等自动内存管理语言的支持正在进行中,不久就会推出。WebAssembly 的设计目的是与 JavaScript 同时运行,使两者能够协同工作。
WebAssembly 程序还可以在浏览器外的环境中运行,这要归功于 WASI(WebAssembly 系统接口),它是 WebAssembly 的模块化系统接口。WASI 可以跨操作系统移植,目的是保证安全,并能在沙箱环境中运行。
WebAssembly 代码(二进制代码,即字节码)能够在跨平台虚拟机(VM)上运行。与 JavaScript 相比,WebAssembly 字节码的解析和执行速度更快,代码表示也更紧凑。
从概念上讲,指令的执行是通过传统的程序计数器(program counter)机制进行的,计数器随着指令的运行递增。在实践中,大多数 Wasm 引擎会将 Wasm 字节码编译为机器码,然后执行机器码。指令可分为两类:
- 控制指令形成控制结构,它会从堆栈中弹出参数值,可能会更改程序计数器,并将结果值推入堆栈。
- 简单指令从堆栈中弹出参数值,将运算符应用于参数值,然后将结果值推入堆栈,随后程序计数器隐式递增。
回到之前的例子,下面的 WebAssembly 代码相当于刚刚的 x86 代码:
|
|
asm.js 仅靠软件就能实现,也就是说其代码可以在任何 JavaScript 引擎中运行(即使未经优化),而 WebAssembly 需要所有浏览器供应商一致同意的新功能才能运行。WebAssembly 于 2015 年公布,2017 年 3 月首次发布,2019 年 12 月 5 日成为 W3C 推荐标准。W3C 维护着 WebAssembly 标准,所有主要浏览器供应商和其他相关方都参与了标准的贡献。从 2017 年以来,WebAssembly 的浏览器支持实现了普及。
WebAssembly 代码有两种格式:文本格式 和 二进制格式。你刚刚看到的代码是文本格式的。
文本表示法
WebAssembly 的文本格式基于 S-expressions,通常使用 .wat
后缀(WebAssembly text format)如果你真的想要的话,也可以手写 .wat
文件。以上面的乘法运算为例,我们可以抽象出一个函数,像这样:
|
|
二进制表示法
使用文件扩展名 .wasm
的二进制格式不适合人类阅读,更不适合人类手动创建。使用类似 wat2wasm 这样的工具,可以将上述代码转换为以下二进制格式。(注释通常不是 .wasm
文件的一部分,这里的注释是由 wat2wasm 工具添加的,以便我们更好地理解代码)。
|
|
编译为 WebAssembly
正如我们刚刚看到的,.wat
和 .wasm
都不太适合人类直接使用,这就是 Emscripten 这样的编译器发挥作用的地方。它可以让你将 C 和 C++ 等高级语言编译为 WebAssembly。此外,还有 Rust 等其他语言的编译器。以下面的 C 代码为例:
|
|
通常,你会用 gcc
编译器来编译这个 C 程序:
|
|
安装 Emscripten 之后,你可以通过 emcc
命令,使用几乎相同的参数来编译出 WebAssembly 程序:
|
|
这将创建 hello.wasm
文件和 HTML 文件 hello.html
。打开 hello.html
,在控制台中你可以看到 "Hello World"
字符串被打印了出来。
我们也可以编译出对应的 JS 文件:
|
|
和刚刚一样,这行命令会创建一个 hello.wasm
文件,还有一个 hello.js
文件,而不是 HTML 文件。我们可以用 Node.js 来运行这个文件:
|
|
了解更多
本文对 WebAssembly 的简要介绍只是冰山一角。有关 WebAssembly 的更多信息,请参阅 MDN 的 WebAssembly 文档和 Emscripten 文档。说实话,使用 WebAssembly 可能有点像 “如何画猫头鹰”(一个外网 meme),毕竟前端工程师通常对于 C 之类的语言比较不熟悉。幸运的是,我们有像 StackOverflow 的 webassembly
标签这样的频道,如果你友好地进行提问,这里的大佬们会很乐意帮助你。