BLOG | NGINX

WebAssembly 组件模型 — 原因、方法和内容(第 1 部分)

NGINX-Part-of-F5-horiz-black-type-RGB
Timo Stark 缩略图
Timo Stark
Published March 13, 2024

如果您有兴趣开始使用 WebAssembly 组件模型,但面对庞大的生态系统不知从何处着手吗?如果是,那么本文正适合您!

在本文中,我们将分享一些经验和心得,它们是我们在向 NGINX Unit 中添加对 WebAssembly 组件模型的支持时所获得的——感谢我们强大而活跃的社区。

如果您已经熟悉 Wasm 生态系统,或者只是想了解如何编写代码,请直接跳转到本系列博文的第二篇

 

WebAssembly 组件模型和 NGINX Unit

在推出第一版 Unit Wasm 语言模块之后,我们做了许多工作。在 2023 年 9 月,我们说过:

我们将 WebAssembly 支持作为技术预览版推出,希望尽快代之以 WASI-HTTP 支持。

我们在 Unit 1.32.0 中做到了这一点。该版本支持将 WASI 0.2 API 和 wasi:http/proxy world 用作其主接口的 Wasm 组件。

此处注意,如果前面有你不熟悉的词汇,也不要担心。本文将介绍 WebAssembly (Wasm) 组件模型的概念,以及 WebAssembly 系统接口 (WASI) 在其中所扮演的角色,还将探讨“WebAssembly 接口类型”的意义。

正如第一篇 Wasm 博文中所述,Wasm 运行时与 Wasm 模块通过共享内存以原始字节的形式共享数据。要理解此字节流,主机和 Wasm 模块必须如出一口,或者从技术上说,实现相同接口。NGINX Unit 的核心概念是创建一个与特定应用相关的 HTTP 请求的上下文,并与运行时共享内存中的这组字节。

这正是我们在 unit-wasm 中所做的。虽然学习如何向 Unit 添加 Wasm 支持既有趣也很有必要,然而这与实现或采用某一标准相去甚远,因此就需要 Wasm 组件模型上场了。

WebAssembly (Wasm) 组件模型明确了不同的 Wasm 模块或组件之间及其与运行时环境之间的通信方式。它建立了必须满足的特定契约,以确保编译到 Wasm 组件中的代码可以托管在兼容的运行时上,并在运行期间与其他 Wasm 组件无缝交换数据。

如需获取这一理论框架的应用示例,请查看 NGINX Unit 中的实现,它是典型的 Wasm 组件模型实例。

Wasm 组件模型的两个重要组成部分是 WebAssembly 系统接口 (WASI) 和 WebAssembly 接口类型 (WIT)。下面我们来详细了解一下这两个标准。

 

WASI 和 WIT

WASI 是“WebAssembly System Interface(WebAssembly 系统接口)”的缩写,由 Wasmtime 项目推出,专为 Wasm 而设计。它是 Wasm 的可移植系统接口,支持访问多项操作系统的功能,包括文件和文件系统、套接字、时钟、随机数等。为什么需要它?

因为我们现在创建的 Wasm 组件是针对服务器端运行时而非基于浏览器的 Wasm 运行时(使用 Web API 或 JavaScript)。浏览器之外的代码需要一种方式来与底层系统通信。为了更好地理解 WASI 的作用,我们用 Rust 编写了一个非常简单的程序,它会输出“Hello World”。

我们编写的代码可以编译成一个可执行的二进制文件。程序启动后,我们会看到命令行上输出“Hello World”。这背后其实是 POSIX 标准在发挥作用,它定义了系统调用。系统调用在不同操作系统上的工作方式不同。

WASI 为这些系统调用提供了一个抽象层,可供编译到 Wasm 的代码使用。兼容 WASI 的运行时能够处理该代码的执行。我们将在本系列博文第二篇的 Rust 教程中进一步介绍其实际应用。自 WASI 提案的 Preview2 开始,WASI-API 在 WIT 文件中进行定义

WIT(Wasm 接口类型)是一种用于定义接口的描述性接口描述语言 (IDL),而非一种通用编码语言。编写的 WIT 文件不包含任何业务逻辑,只是纯粹的契约定义。多个接口可以进一步组合为 ”world”。虽然深入了解如何创建自己的 WIT 文件不是必需的,但有助于在构建组件时查找问题或排除故障。如欲了解有关 WIT 编程语言的更多信息,请参阅官方文档

Wasm 组件模型和 wasi:http/proxy world 使用的 WIT 文件由字节码联盟 (Bytecode Alliance) 创建和维护。截至本文撰写之时,使用它们的最佳方式是通过 Wasmtime 项目的 GitHub 代码库,以及进行手动拉取。

WIT 文件的一个有趣之处在于其版本控制系统。由于实现 Wasm 运行时的主机以及我们将要构建的组件都在为 WIT 文件定义的契约创建绑定,因此我们必须指向这些契约的同一版本,或者选择支持多个 WIT 文件版本的运行时。这值得再写一篇博文。现在,我们只考虑最新的稳定版本,它发布于 2024 年 2 月,标记为 WASI 0.2。该版本包括 wasi:cli world 和 wasi:http world。

在 WebAssembly 生态系统中,这些契约被称为“world”,下文将使用这一术语。就 NGINX Unit 用例而言,我们的目标非常明确,那就是 wasi:http/proxy world。您可以将 wasi:http/proxy world 视为一组描述 HTTP 请求和响应的接口,包括所有数据(HTTP 方法、请求头、正文等)。如果您是老 Web 开发人员,这可能会让您想起 CGI。

 

NGINX Unit、Wasmtime 和 Rust — 运行时实现

经过上面的介绍,我们现在知道 WASI/WIT 在支持组件模型方面扮演重要角色。作为站点,Unit 必须实现 WIT 文件定义的 WASI HTTP 代理接口才能履行契约。对此我们早已知晓。那既然使用 Wasmtime 作为 Wasm 运行时,我们何不将此任务委托给运行时呢?当然,完全可以!不过,有一个虽不起眼但很重要的细节:我们当时的实现完全是用 C 语言编写的,使用的是 Wasmtime C-API。遗憾的是,这些 API 缺乏支持组件模型的必要功能。

正如本文开头所述,只要找到对的人,心往一处想,再复杂的挑战均可迎刃而解。无论过去还是现在,Fermyon 都是我们极具价值的重要合作伙伴。经过一场 Slack 和 Zoom 深夜会议后,我们发现在 Wasmtime C-API 中添加对组件模型的原生支持过于复杂。此外,没有 bindgen 等自动化工具的帮助,使用 WIT 文件手动实现接口将需要大量维护工作。

在向 Fermyon 解释 NGINX Unit 的内部结件和当前基于 C 语言的语言模块的工作原理后,他们分享了一个支持 Wasmtime 的 Rust API 的基于 Rust 的 Unit 语言模块原型。再没有 C-API 的什么事了。

现在,我们准备好开始写代码了。

 

后续

在下一篇中,我们将介绍使用 Rust 和 WASI 0.2 API 创建 Wasm 组件的流程。

第 2 部分


"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."