WebAssembly 概念

本文解释了 WebAssembly 的工作原理概念,包括其目标、它解决的问题以及它如何在 Web 浏览器的 JavaScript 引擎中运行。

什么是 WebAssembly?

WebAssembly 是一种可以在现代 Web 浏览器中运行的新型代码,提供新功能和显著的性能提升。它并非主要用于手写,而是旨在成为 C、C++、Rust 等源语言的有效编译目标。

这对于 Web 平台具有巨大的影响——它提供了一种以接近原生速度在 Web 上运行多种语言编写的代码的方式,使得以前无法在 Web 上运行的客户端应用程序得以实现。

更重要的是,你甚至不需要知道如何创建 WebAssembly 代码就可以利用它。WebAssembly 模块可以导入到 Web(或 Node.js)应用程序中,通过 JavaScript 暴露 WebAssembly 函数供使用。JavaScript 框架可以利用 WebAssembly 来带来巨大的性能优势和新功能,同时仍然使功能易于 Web 开发人员使用。

WebAssembly 的目标

WebAssembly 正在 W3C WebAssembly 社区组 内作为开放标准创建,目标如下:

  • 快速、高效且可移植 — WebAssembly 代码可以利用 通用硬件功能,在不同平台上以接近原生的速度执行。
  • 可读且可调试 — WebAssembly 是一种低级汇编语言,但它确实具有人类可读的文本格式(其规范仍在最终确定中),允许手动编写、查看和调试代码。
  • 保持安全 — WebAssembly 被指定在安全的沙盒执行环境中运行。与其他 Web 代码一样,它将强制执行浏览器的同源和权限策略。
  • 不破坏 Web — WebAssembly 的设计使其能与其他 Web 技术良好地配合,并保持向后兼容性。

注意: WebAssembly 也将在 Web 和 JavaScript 环境之外使用(参见 非 Web 嵌入)。

WebAssembly 如何融入 Web 平台?

Web 平台可以被认为是两部分:

  • 一个运行 Web 应用程序代码的虚拟机(VM),例如,驱动你的应用程序的 JavaScript 代码。
  • 一组 Web API,Web 应用程序可以调用它们来控制 Web 浏览器/设备功能并实现各种操作(DOMCSSOMWebGLIndexedDBWeb Audio API 等)。

历史上,虚拟机只能加载 JavaScript。这对于我们来说一直运作良好,因为 JavaScript 强大到足以解决当今 Web 上人们遇到的大多数问题。然而,当试图将 JavaScript 用于更密集的用例时,我们遇到了性能问题,例如 3D 游戏、虚拟和增强现实、计算机视觉、图像/视频编辑以及许多其他需要原生性能的领域(有关更多想法,请参见 WebAssembly 用例)。

此外,下载、解析和编译非常大的 JavaScript 应用程序的成本可能高得令人望而却步。移动和其他资源受限平台可能会进一步放大这些性能瓶颈。

WebAssembly 是一种不同于 JavaScript 的语言,但它并非旨在取代 JavaScript。相反,它旨在补充 JavaScript 并与 JavaScript 协同工作,允许 Web 开发人员利用两种语言的优势:

  • JavaScript 是一种高级语言,足够灵活和富有表现力,可以编写 Web 应用程序。它具有许多优点——它是动态类型的,不需要编译步骤,并且拥有庞大的生态系统,提供强大的框架、库和其他工具。
  • WebAssembly 是一种低级汇编式语言,具有紧凑的二进制格式,以接近原生的性能运行,并为具有低级内存模型(如 C++ 和 Rust)的语言提供编译目标,以便它们可以在 Web 上运行。(请注意,WebAssembly 的 高级目标 是在未来支持具有垃圾回收内存模型的语言。)

随着 WebAssembly 在浏览器中出现,我们之前讨论过的虚拟机现在将加载和运行两种类型的代码——JavaScript 和 WebAssembly。

不同的代码类型可以根据需要相互调用——WebAssembly JavaScript API 将导出的 WebAssembly 代码封装在可以正常调用的 JavaScript 函数中,WebAssembly 代码可以导入并同步调用普通的 JavaScript 函数。事实上,WebAssembly 代码的基本单元称为模块,WebAssembly 模块在许多方面与 ES 模块对称。

WebAssembly 核心概念

理解 WebAssembly 如何在浏览器中运行需要几个核心概念。所有这些概念都与 WebAssembly JavaScript API 中的概念一一对应。

  • 模块 (Module):表示一个由浏览器编译成可执行机器代码的 WebAssembly 二进制文件。模块是无状态的,因此,像 Blob 一样,可以通过 postMessage() 在窗口和 worker 之间显式共享。模块声明导入和导出,就像 ES 模块一样。
  • 内存 (Memory):一个可调整大小的 ArrayBuffer,包含 WebAssembly 低级内存访问指令读写的一维字节数组。
  • 表 (Table):一个可调整大小的引用类型数组(例如,对函数的引用),这些引用出于安全和可移植性原因无法以原始字节形式存储在内存中。
  • 实例 (Instance):一个模块与它在运行时使用的所有状态(包括内存、表和一组导入值)的组合。实例就像一个已加载到特定全局范围并带有一组特定导入的 ES 模块。

JavaScript API 为开发人员提供了创建模块、内存、表和实例的能力。给定一个 WebAssembly 实例,JavaScript 代码可以同步调用其导出,这些导出暴露为普通的 JavaScript 函数。WebAssembly 代码还可以通过将这些 JavaScript 函数作为导入传递给 WebAssembly 实例来同步调用任意 JavaScript 函数。

由于 JavaScript 完全控制 WebAssembly 代码的下载、编译和运行方式,JavaScript 开发人员甚至可以将 WebAssembly 视为 JavaScript 的一个功能,用于高效生成高性能函数。

将来,WebAssembly 模块将能够像 ES 模块一样加载(使用 <script type='module'>),这意味着 JavaScript 将能够像加载 ES 模块一样轻松地获取、编译和导入 WebAssembly 模块。

我如何在我的应用程序中使用 WebAssembly?

上面我们讨论了 WebAssembly 为 Web 平台添加的原始基元:代码的二进制格式以及加载和运行这种二进制代码的 API。现在让我们讨论如何在实践中使用这些基元。

WebAssembly 生态系统正处于初期阶段;未来无疑将出现更多工具。目前,有四个主要切入点:

  • 使用 Emscripten 移植 C/C++ 应用程序。
  • 直接在汇编级别编写或生成 WebAssembly。
  • 编写 Rust 应用程序并以 WebAssembly 作为其输出。
  • 使用 AssemblyScript,它类似于 TypeScript 并编译为 WebAssembly 二进制文件。

我们来谈谈这些选项:

从 C/C++ 移植

创建 Wasm 代码的众多选项之一是使用在线 Wasm 汇编器或 Emscripten。有许多在线 Wasm 汇编器可供选择,例如:

对于那些试图入门的人来说,这些都是很好的资源,但它们缺乏 Emscripten 的一些工具和优化。

Emscripten 工具能够将几乎任何 C/C++ 源代码编译成 Wasm 模块,以及加载和运行该模块所需的 JavaScript “胶水”代码,以及一个用于显示代码结果的 HTML 文档。

Diagram: Emscripten compiles C/C++ source code and into a Wasm module, an HTML document along with the JavaScript glue code.

简而言之,该过程如下:

  1. Emscripten 首先将 C/C++ 代码输入到 clang+LLVM——一个成熟的开源 C/C++ 编译器工具链,例如作为 macOS 上 Xcode 的一部分发布。
  2. Emscripten 将 clang+LLVM 的编译结果转换为 Wasm 二进制文件。
  3. 目前,WebAssembly 本身不能直接访问 DOM;它只能调用 JavaScript,并传递整数和浮点基本数据类型。因此,要访问任何 Web API,WebAssembly 需要调用 JavaScript,然后 JavaScript 进行 Web API 调用。因此,Emscripten 会创建实现此目的所需的 HTML 和 JavaScript 胶水代码。

注意: 未来计划 允许 WebAssembly 直接调用 Web API

JavaScript 胶水代码并不像你想象的那么简单。首先,Emscripten 实现了流行的 C/C++ 库,例如 SDLOpenGLOpenAL 和部分 POSIX。这些库是根据 Web API 实现的,因此每个库都需要一些 JavaScript 胶水代码来将 WebAssembly 连接到底层 Web API。

因此,胶水代码的一部分是实现 C/C++ 代码使用的每个相应库的功能。胶水代码还包含调用上述 WebAssembly JavaScript API 以获取、加载和运行 Wasm 文件的逻辑。

生成的 HTML 文档加载 JavaScript 胶水文件并将标准输出写入 <textarea>。如果应用程序使用 OpenGL,HTML 还包含一个 <canvas> 元素,用作渲染目标。修改 Emscripten 输出并将其转换为你需要的任何 Web 应用程序都非常容易。

你可以在 emscripten.org 找到 Emscripten 的完整文档,并在 从 C/C++ 编译到 WebAssembly 中找到实现工具链并将你自己的 C/C++ 应用程序编译到 Wasm 的指南。

直接编写 WebAssembly

你想构建自己的编译器,或自己的工具,或创建一个在运行时生成 WebAssembly 的 JavaScript 库吗?

与物理汇编语言类似,WebAssembly 二进制格式具有文本表示形式——两者之间存在一对一的对应关系。你可以手动编写或生成这种格式,然后使用几种 WebAssembly 文本到二进制工具 将其转换为二进制格式。

有关如何执行此操作的简单指南,请参阅我们的 将 WebAssembly 文本格式转换为 Wasm 文章。

编写面向 WebAssembly 的 Rust

由于 Rust WebAssembly 工作组的辛勤工作,也可以编写 Rust 代码并编译到 WebAssembly。你可以从安装必要的工具链开始,将示例 Rust 程序编译为 WebAssembly npm 包,并在示例 Web 应用程序中使用它,请参阅我们的 从 Rust 编译到 WebAssembly 文章。

使用 AssemblyScript

对于希望尝试 WebAssembly 但又不想学习 C 或 Rust 细节的 Web 开发人员来说,AssemblyScript 将是最佳选择,它让他们可以在熟悉的 TypeScript 等语言环境中工作。AssemblyScript 将 TypeScript 的严格变体编译为 WebAssembly,允许 Web 开发人员继续使用他们熟悉的 TypeScript 兼容工具——例如 Prettier、ESLint、VS Code IntelliSense 等。你可以在 https://assemblyscript.webassembly.net.cn/ 上查看其文档。

总结

本文向你解释了 WebAssembly 是什么,为什么它如此有用,它如何融入 Web,以及你如何利用它。

另见