自从网络不仅成为文档平台,也成为应用平台以来,一些最先进的应用已将网络浏览器推到了极限。在许多高级语言中,都会采用通过与低级语言交互来“贴近硬件”以提高性能的方法。例如,Java 具有 Java 原生接口。对于 JavaScript,这种低级语言是 WebAssembly。在本文中,您将了解什么是汇编语言,以及它为什么能在 Web 上发挥作用,然后学习如何通过 asm.js 的临时解决方案创建 WebAssembly。
汇编语言
您是否曾使用汇编语言进行编程?在计算机编程中,汇编语言通常简称为汇编语言,通常缩写为 ASM 或 asm,是任何低阶编程语言,语言指令与架构的机器代码指令之间存在极强的对应关系。
例如,查看 Intel® 64 和 IA-32 架构 (PDF),MUL
指令(用于乘)会对第一个操作数(目标操作数)和第二个操作数(源操作数)执行无符号乘法,并将结果存储在目标操作数中。简而言之,目标操作数是位于寄存器 AX
中的隐式操作数,而源操作数位于通用寄存器(如 CX
)中。结果再次存储在寄存器 AX
中。请参考以下 x86 代码示例:
mov ax, 5 ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx ; Multiply the value of register AX (5)
; and the value of register CX (10), and
; store the result in register AX.
举个例子,如果任务是将 5 和 10 相乘,您可能会在 JavaScript 中编写类似于以下代码的代码:
const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;
采用汇编方式的优势在于,这种经过机器优化的低级代码比经过人工优化的高级代码效率要高得多。在前面的示例中,这并不重要,但您可以想象,对于更复杂的操作,差异可能会很大。
顾名思义,x86 代码依赖于 x86 架构。如果有一种汇编代码不依赖于特定架构,但可以继承汇编的性能优势,该怎么办?
asm.js
编写没有架构依赖项的汇编代码的第一步是 asm.js,它是 JavaScript 的一个严格子集,可用作编译器的低层级高效目标语言。此子语言有效地描述了适用于 C 或 C++ 等非内存安全型语言的沙盒化虚拟机。通过结合使用静态和动态验证,JavaScript 引擎可以针对有效的 asm.js 代码采用提前优化编译策略 (AOT)。使用手动内存管理的静态类型语言(例如 C)编写的代码由源到源编译器进行翻译,例如 early Emscripten(基于 LLVM)。
通过将语言功能限制为适合 AOT 的功能,提升了性能。Firefox 22 是首款支持 asm.js 的浏览器,该版本以 OdinMonkey 的名义发布。Chrome 在 61 版中添加了 asm.js 支持。虽然 asm.js 仍可在浏览器中运行,但已被 WebAssembly 取代。此时使用 asm.js 的原因是,可以替代不支持 WebAssembly 的浏览器。
WebAssembly
WebAssembly 是一种类似于汇编的低级语言,采用紧凑的二进制格式,运行时性能接近原生,并提供 C/C++、Rust 等语言以及许多其他语言,这些语言都具有编译目标,可在 Web 上运行。我们正在开发对 Java 和 Dart 等内存管理语言的支持,并且很快就会推出,或者已经像 Kotlin/Wasm 一样推出。WebAssembly 旨在与 JavaScript 一起运行,让二者能够协同工作。
得益于 WASI(WebAssembly System Interface,即 WebAssembly 的模块化系统界面),除了浏览器之外,WebAssembly 程序还可以在其他运行时内运行。WASI 旨在实现跨操作系统的可移植性,并确保安全性和能够在沙盒环境中运行。
WebAssembly 代码(二进制代码,即字节码)旨在在便携式虚拟堆栈机 (VM) 上运行。与 JavaScript 相比,字节码的解析和执行速度更快,并且代码表示形式更紧凑。
指令的概念执行是通过传统的程序计数器进行的,该计数器会按指令顺序推进。在实践中,大多数 Wasm 引擎都会将 Wasm 字节码编译为机器码,然后执行该机器码。相关说明分为两类:
- 用于构成控制结构并从堆栈中弹出其参数值的控制指令可能会更改程序计数器,并将结果值推送到堆栈。
- 简单指令:从堆栈中弹出其参数值,对值应用运算符,然后将结果值推送到堆栈,然后隐式推进程序计数器。
回到前面的示例,以下 WebAssembly 代码相当于本文开头部分的 x86 代码:
i32.const 5 ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul ; Pop the two most recent items on the stack,
; multiply them, and push the result onto the stack.
虽然 asm.js 完全通过软件实现,也就是说,其代码可以在任何 JavaScript 引擎中运行(即使未经过优化),但 WebAssembly 需要所有浏览器供应商都同意的新功能。WebAssembly 于 2015 年宣布,并于 2017 年 3 月首次发布,于 2019 年 12 月 5 日成为 W3C 推荐标准。W3C 会维护该标准,并接受所有主要浏览器供应商和其他利益相关方的贡献。自 2017 年起,浏览器支持范围涵盖所有主流浏览器。
WebAssembly 有两种表示法:文本和二进制。您在上面看到的是文本表示法。
文本表示法
文本表示法基于 S 表达式,通常采用文件扩展名 .wat
(适用于网络组合文字格式)。如果您真的想这样做,可以手写。我们将上面的乘法示例改为不再对因子进行硬编码,使其更实用,这样您就可以理解以下代码了:
(module
(func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
local.get $factor1
local.get $factor2
i32.mul)
(export "mul" (func $mul))
)
二进制表示法
使用文件扩展名 .wasm
的二进制格式不适合供人使用,更不用说由人创建。使用 wat2wasm 等工具,您可以将上述代码转换为以下二进制表示法。(这些注释通常不是二进制表示法的一部分,而是由 wat2wasm 工具添加的,以便更好地理解。)
0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0100 0000 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01 ; section code
0000009: 00 ; section size (guess)
000000a: 01 ; num types
; func type 0
000000b: 60 ; func
000000c: 02 ; num params
000000d: 7f ; i32
000000e: 7f ; i32
000000f: 01 ; num results
0000010: 7f ; i32
0000009: 07 ; FIXUP section size
; section "Function" (3)
0000011: 03 ; section code
0000012: 00 ; section size (guess)
0000013: 01 ; num functions
0000014: 00 ; function 0 signature index
0000012: 02 ; FIXUP section size
; section "Export" (7)
0000015: 07 ; section code
0000016: 00 ; section size (guess)
0000017: 01 ; num exports
0000018: 03 ; string length
0000019: 6d75 6c mul ; export name
000001c: 00 ; export kind
000001d: 00 ; export func index
0000016: 07 ; FIXUP section size
; section "Code" (10)
000001e: 0a ; section code
000001f: 00 ; section size (guess)
0000020: 01 ; num functions
; function body 0
0000021: 00 ; func body size (guess)
0000022: 00 ; local decl count
0000023: 20 ; local.get
0000024: 00 ; local index
0000025: 20 ; local.get
0000026: 01 ; local index
0000027: 6c ; i32.mul
0000028: 0b ; end
0000021: 07 ; FIXUP func body size
000001f: 09 ; FIXUP section size
; section "name"
0000029: 00 ; section code
000002a: 00 ; section size (guess)
000002b: 04 ; string length
000002c: 6e61 6d65 name ; custom section name
0000030: 01 ; name subsection type
0000031: 00 ; subsection size (guess)
0000032: 01 ; num names
0000033: 00 ; elem index
0000034: 03 ; string length
0000035: 6d75 6c mul ; elem name 0
0000031: 06 ; FIXUP subsection size
0000038: 02 ; local name type
0000039: 00 ; subsection size (guess)
000003a: 01 ; num functions
000003b: 00 ; function index
000003c: 02 ; num locals
000003d: 00 ; local index
000003e: 07 ; string length
000003f: 6661 6374 6f72 31 factor1 ; local name 0
0000046: 01 ; local index
0000047: 07 ; string length
0000048: 6661 6374 6f72 32 factor2 ; local name 1
0000039: 15 ; FIXUP subsection size
000002a: 24 ; FIXUP section size
编译为 WebAssembly
如您所见,.wat
和 .wasm
都不是特别人性化。这正是 Emscripten 等编译器的用武之地。它支持从 C 和 C++ 等高级语言进行编译。还有适用于 Rust 等其他语言的其他编译器。请考虑以下 C 代码:
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
通常,您可以使用编译器 gcc
编译此 C 程序。
$ gcc hello.c -o hello
安装 Emscripten 后,您可以使用 emcc
命令和几乎相同的参数将其编译为 WebAssembly:
$ emcc hello.c -o hello.html
这将创建一个 hello.wasm
文件和 HTML 封装容器文件 hello.html
。从网络服务器提供文件 hello.html
时,您会看到开发者工具控制台中输出了 "Hello World"
。
还有一种方法可以在不使用 HTML 封装容器的情况下编译为 WebAssembly:
$ emcc hello.c -o hello.js
与之前一样,这将创建一个 hello.wasm
文件,但这次创建的是 hello.js
文件,而不是 HTML 封装容器。为了进行测试,您可以使用 Node.js 等工具运行生成的 JavaScript 文件 hello.js
:
$ node hello.js
Hello World
了解详情
这只是 WebAssembly 的简要介绍,冰山一角。
如需详细了解 WebAssembly,请参阅 MDN 的 WebAssembly 文档,并参阅 Emscripten 文档。老实说,使用 WebAssembly 可能会让人感觉有点像如何绘制猫头鹰表情包,特别是因为熟悉 HTML、CSS 和 JavaScript 的网络开发者不一定精通 C 等语言进行编译。幸运的是,像 StackOverflow 的 webassembly
标签这样的渠道,只要你提出好要求,专家通常很乐意为你提供帮助。
致谢
本文由 Jakob Kummerow、Derek Schuff 和 Rachel Andrew 审核。