什么是 WebAssembly,它从何而来?

自从网络不仅成为了文档平台,还成为了应用的平台,一些最先进的应用程序已让网络浏览器达到了其极限。通过与低阶语言进行交互来提升性能,许多高级语言都采用了这种“更接近金属”的方法。例如,Java 具有 Java 原生接口。对于 JavaScript,这种较低级别的语言为 WebAssembly。在本文中,您将了解什么是汇编语言,以及为什么它在网络上非常有用,以及如何通过 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 引擎可以采用预先 (AOT) 优化编译策略来获取有效的 asm.js 代码。使用静态类型的语言编写的代码需要手动管理内存(例如 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 程序还可在其他运行时中运行,这得益于 WASI(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 封装容器。为了进行测试,您可以运行生成的 JavaScript 文件 hello.js,例如使用 Node.js:

$ node hello.js
Hello World

了解详情

这段 WebAssembly 简介只是冰山一角。如需详细了解 WebAssembly,请参阅关于 MDN 的 WebAssembly 文档,并参阅 Emscripten 文档。事实上,使用 WebAssembly 时,感觉有点像如何绘制猫头鹰表情包,尤其是因为熟悉 HTML、CSS 和 JavaScript 的 Web 开发者并不一定精通 C 这样的待编译语言。幸运的是,你可以通过 StackOverflow 的 webassembly 标签等渠道找到专家,如果您问得合适的问题,专家们通常很乐意为您提供帮助。

致谢

本文由 Jakob KummerowDerek SchuffRachel Andrew 审核。