自从网络不仅成为文档平台,也成为应用平台以来,一些最先进的应用已将网络浏览器推到了极限。许多高级别的语言都遇到通过与较低级别的语言进行交互以“贴近金属”来提高性能的方法。例如,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)编写且采用手动内存管理的代码由源代码到源代码编译器(例如早期 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 一起运行,使这两者可以协同工作。
除了浏览器之外,WebAssembly 程序还可以在其他运行时中运行,这得益于 WebAssembly 系统接口 (WASI),它是 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
(适用于 WebAssembly text 格式)。如果确实需要,您可以手写输入。我们将上面的乘法示例改为不再对因子进行硬编码,使其更实用,这样您就可以理解以下代码了:
(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
。从 Web 服务器传送文件 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 有点像在使用 How to draw an owl meme(如何画猫头鹰表情包),尤其是因为熟悉 HTML、CSS 和 JavaScript 的 Web 开发者不一定精通 C 等要编译的语言。幸运的是,有 StackOverflow 的 webassembly
标签等渠道,如果您礼貌地提出问题,专家通常会很乐意提供帮助。
致谢
本文由 Jakob Kummerow、Derek Schuff 和 Rachel Andrew 审核。