本文记录从复刻项目出发,对 Wasm Component Model 是否适合作为通用插件系统基础的一次验证性学习。
系列前言与声明
最近 dsh 的讨论度很高。我最初的想法很朴素:尝试复刻它,并借这个过程学习一个真实的插件系统应当怎样划分宿主、扩展和能力边界。阅读并整理本地 cordis 工程时,我发现现有插件主要以 TypeScript 编写。这条路线与 JavaScript 生态结合紧密,也足够直接;但它同时引出了一个我想继续追问的问题:如果未来希望 Rust、TypeScript 或其他语言实现同一类插件,宿主还能否安全、稳定地加载它们?
这不是要否定 TypeScript 插件,也不是预先认定 Wasm 必然优于其他方案。动态库、RPC、容器和解释器插件各有适用范围。本文关注的仅是一个具体假设:能否利用 Wasm Component Model 的 WIT 接口、Canonical ABI 和能力模型,构建一个由宿主控制权限、允许多语言实现的插件边界。
选择 Wasm Component Model 的原因在于,它提供了一个值得验证的组合:**用 WIT 定义跨语言接口,用 Component 交付实现,用 Host 显式提供能力。**如果这一组合能够覆盖文本处理、规则执行等本地应用插件场景,后续再讨论是否值得扩展到更复杂的产品;如果不能,仓库中的构建结果和限制也应当成为结论的一部分。
因此,本系列的当前目的不是发布一个生产级插件框架,而是建立一条可运行、可检查的学习链路,逐步验证以下问题:
- 同一份 WIT 能否同时约束 Rust 与 TypeScript Guest;
- Wasmtime Host 能否以一致方式加载两种 Component;
- Host 能否只向插件提供必要的输入、配置和标准能力;
- 类型生成和端到端调用能否及时暴露接口漂移;
- 现阶段工具链有哪些明确限制,特别是资源管理和异步 I/O。
作者同样处于学习与调研过程中。Component Model、WASI 和相关工具链仍在快速演进,本文中的代码、命令和判断以撰写时仓库锁定的依赖版本为准;它们不构成标准规范的替代,也不保证在其他版本或其他运行时中具有完全相同的行为。涉及语法和规范语义时,应优先以官方文档为准;文章将尽量标明实践结论与规范事实的边界,并在发现错误后更新。
本文是系列第一篇。读完后你应能回答:Wasm、Component、WIT、WASI、Wasmtime 分别是什么;它们在插件系统里各司什么职;以及为什么还需要 Canonical ABI。后两篇会分别让 Rust 和 TypeScript 实现同一个 WIT 插件,并交给同一个 Host 验证。
贯穿场景:可扩展的本地文本工具
为了避免依赖 Web 服务知识,系列将 Host 视为一个本地文本工具:用户从命令行选择文本或文件,Host 读取输入与配置,调用插件完成格式化、内容检查或文件命名建议,再显示或保存结果。
在这个场景中,Host 负责命令参数、文件 I/O、配置和日志;插件只处理 WIT 明确声明的数据。仓库当前使用 greet 作为最小接口,是为了将注意力集中在构建、绑定和实例化过程。理解链路后,可将它自然扩展为 format(text)、validate(content) 等文本处理接口。
目标架构:一个接口、两种实现、一个 Host
仓库的最小契约只有一件事:问候一个名字。
wit
package wasm-component-lab:greeter@0.1.0;
interface api {
greet: func(name: string) -> string;
}
world plugin {
export api;
}
同一份 world plugin 可以由 Rust 或 TypeScript 实现。根目录 Rust Host 不关心组件的源语言,只按 WIT 调用 api.greet。
在仓库根目录运行:
powershell
rustup target add wasm32-wasip2
cargo xtask check
该命令依次构建并加载 Rust 与 TypeScript 组件,再输出各自的问候语。这是后两篇将实现的完整调用链路。
适用问题与技术边界
Component Model 不只是 Wasm 的封装格式。它主要提供跨语言、可版本化、可组合的接口模型。
| 方案 | 适合什么 | 常见代价 |
|---|---|---|
| 动态库 / FFI | 同语言或固定 ABI 的高性能扩展 | 平台、编译器和内存模型耦合强 |
| HTTP / gRPC | 独立部署的服务 | 网络、序列化和运维成本较高 |
| JavaScript 插件 | JS 生态内扩展 | 难自然复用到 Rust、Go 等语言 |
| Wasm Component + WIT | 多语言进程内插件、可控能力边界 | 工具链仍在演进,需要维护协议版本 |
典型场景包括:
- 命令行工具中的文本格式化、文件命名、内容检查或工作流步骤;
- 桌面应用中的可替换规则与受控扩展;
- 同一插件接口的 Rust/TS 多语言实现和互操作测试;
- 将应用中可替换的处理逻辑从用户界面和本地 I/O 中分离出来。
它不适合所有问题。需要共享大量可变内存、依赖完整 Node 原生模块、追求极小二进制或极低延迟的数值内核时,直接 Rust crate、原生扩展或独立服务往往更简单。
核心概念
1. Core Wasm:底层执行格式与类型边界
传统 Wasm module 的函数边界主要是 i32、i64、f32、f64。string、list、result 之类值最终都要落到线性内存和整数参数上。因此两个不同语言的 module 即便都"能传字符串",也可能在指针、长度、释放责任和错误表示上不兼容。
2. WIT:先定义接口,再实现功能
WIT(WebAssembly Interface Type)是 Component Model 的接口定义语言。它不写业务逻辑,只声明 package、接口、类型和 world。
interface:一组相关能力,例如api、configuration;world:某个组件完整的 import/export 接口边界;import:组件需要 Host 或其他组件提供什么;export:组件对外提供什么;package与版本:协议身份,而非文件夹名称。
将 WIT 放在仓库根目录并让 Host 与所有 Guest 都从这里生成绑定,是防止接口漂移的核心工程规则。
3. Component:具有类型化接口的 Wasm 封装单元
Component 可以包含一个或多个 Core Wasm module,并声明其导入和导出的类型化接口。Host 在实例化时检查所需 import 是否被满足,调用方也可以依据生成绑定调用 export。
这就是"Rust 实现能被 Rust Host 调用、TS 实现也能被同一 Host 调用"的前提。
4. Canonical ABI:跨语言值转换规范
Canonical ABI 是 Component Model 的标准转换规则。它规定 WIT 的 string、record、variant、list<u8>、resource 等,如何 lower 到 Core Wasm 的整数、线性内存与调用约定,再如何 lift 回目标语言的自然类型。
它为跨语言调用定义了一致的值转换和调用约定:
业务代码通常不必手写指针和长度;wit-bindgen、Wasmtime 绑定与 ComponentizeJS 会生成相应的适配代码。也正因此,WIT 类型检查通过后仍需要跨语言冒烟测试:它验证完整的生成链路与 ABI,而不只是源代码。
5. WASI 与 Wasmtime:系统能力与宿主运行时
WASI 是一组标准化的系统接口,不等于"给 Wasm 一台完整电脑"。文件系统、环境变量、时钟、网络、标准输出等能力是否可用,由 Host 决定。
Wasmtime 是一个可嵌入的 Wasm/Component 运行时。作为 Host,它负责:
- 读取并编译 Component;
- 为 WIT imports 提供实现;
- 创建每次调用的
Store<HostState>; - 只授予必要的 WASI 和业务能力;
- 调用组件 export,并处理 trap 与业务错误。
例如示例 Host 仅继承标准输入输出,不能因为组件里有 println! 就推导出它能够读取本地文件或访问网络。
Host 与 Guest:import/export 的方向
从组件视角判断方向:
| WIT 声明 | 组件含义 | Host 要做什么 |
|---|---|---|
export api |
"我提供 api" | 调用生成的 call_* 包装 |
import configuration |
"我需要配置" | 在 Linker 中提供实现 |
在命令行或桌面应用中,用户输入、文件访问、应用配置和日志设施应由 Host 持有;Guest 仅获取明确且受限的 WIT capability 或已准备好的数据结构,而不应直接获取文件句柄或完整应用状态。
推荐的学习顺序
接下来两篇都围绕当前仓库,不要求另建 toy project:
- 第二篇:Rust Guest + Wasmtime Host:先走工具链最成熟的路径;
- 第三篇:TypeScript Guest 工程化:理解为什么 TS 需要生成声明,以及如何用
satisfies防止接口漂移。