WebAssembly 概念

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

什么是 WebAssembly?

WebAssembly 是一种新型代码,可以在现代 Web 浏览器中运行,并提供新功能和性能上的重大提升。它主要不是为了手动编写,而是被设计为 C、C++、Rust 等源语言的有效编译目标。

这对 Web 平台具有重大意义——它提供了一种方法,可以在 Web 上以接近本机的速度运行用多种语言编写的代码,使以前无法在 Web 上运行的客户端应用程序能够运行。

更重要的是,您甚至无需了解如何创建 WebAssembly 代码即可利用它。WebAssembly 模块可以导入到 Web(或 Node.js)应用程序中,公开 WebAssembly 函数以供通过 JavaScript 使用。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 应用程序可以调用这些 API 来控制 Web 浏览器/设备功能并使事情发生 (DOMCSSOMWebGLIndexedDBWeb 音频 API 等)。

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

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

WebAssembly 是一种与 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 中 1:1 地反映出来。

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

JavaScript API 为开发人员提供了创建模块、内存、表和实例的能力。在给定 WebAssembly 实例的情况下,JavaScript 代码可以同步调用其导出项,这些导出项作为普通的 JavaScript 函数公开。任意 JavaScript 函数也可以通过将这些 JavaScript 函数作为导入传递给 WebAssembly 实例,由 WebAssembly 代码同步调用。

由于 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++ 编译器工具链,例如,作为 OSX 上 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 文章中开始安装必要的工具链、将示例 Rust 程序编译为 WebAssembly npm 包,并在示例 Web 应用程序中使用它。

使用 AssemblyScript

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

总结

本文解释了 WebAssembly 是什么,为什么它如此有用,它如何适应 Web 以及如何使用它。

另请参阅