从 dsh 的插件系统出发:为什么我要探索 WIT 与 Wasm Component Model

本文记录从复刻项目出发,对 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

flowchart LR W["WIT:world plugin"] --> RG[Rust Guest] W --> TG[TypeScript Guest] RG --> RC[Rust Component] TG --> TC[TS Component] W --> H["Wasmtime Host"] RC --> H TC --> H H --> A["api.greet(name)"]

在仓库根目录运行:

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、原生扩展或独立服务往往更简单。

核心概念

flowchart TB Core["Core Wasm\n只有数值类型和线性内存"] WIT["WIT\n接口定义语言"] ABI["Canonical ABI\n把 WIT 值转换为底层表示"] Component["Component\n带有类型化 import/export 的交付物"] WASI["WASI\n可选的标准系统能力"] Runtime["Wasmtime / Host\n实例化、链接、授权、调用"] WIT --> ABI Core --> Component ABI --> Component WASI --> Runtime Component --> Runtime

1. Core Wasm:底层执行格式与类型边界

传统 Wasm module 的函数边界主要是 i32i64f32f64stringlistresult 之类值最终都要落到线性内存和整数参数上。因此两个不同语言的 module 即便都"能传字符串",也可能在指针、长度、释放责任和错误表示上不兼容。

2. WIT:先定义接口,再实现功能

WIT(WebAssembly Interface Type)是 Component Model 的接口定义语言。它不写业务逻辑,只声明 package、接口、类型和 world。

  • interface:一组相关能力,例如 apiconfiguration
  • 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 的 stringrecordvariantlist<u8>resource 等,如何 lower 到 Core Wasm 的整数、线性内存与调用约定,再如何 lift 回目标语言的自然类型。

它为跨语言调用定义了一致的值转换和调用约定:

sequenceDiagram participant H as Rust Host: String participant A as Canonical ABI participant G as TS/Rust Guest H->>A: greet(&#34;Ada&#34;) A->>G: lower 为底层 Wasm 调用 G->>A: 返回 WIT string A->>H: lift 为 Rust String

业务代码通常不必手写指针和长度;wit-bindgen、Wasmtime 绑定与 ComponentizeJS 会生成相应的适配代码。也正因此,WIT 类型检查通过后仍需要跨语言冒烟测试:它验证完整的生成链路与 ABI,而不只是源代码。

5. WASI 与 Wasmtime:系统能力与宿主运行时

WASI 是一组标准化的系统接口,不等于"给 Wasm 一台完整电脑"。文件系统、环境变量、时钟、网络、标准输出等能力是否可用,由 Host 决定。

Wasmtime 是一个可嵌入的 Wasm/Component 运行时。作为 Host,它负责:

  1. 读取并编译 Component;
  2. 为 WIT imports 提供实现;
  3. 创建每次调用的 Store<HostState>
  4. 只授予必要的 WASI 和业务能力;
  5. 调用组件 export,并处理 trap 与业务错误。

例如示例 Host 仅继承标准输入输出,不能因为组件里有 println! 就推导出它能够读取本地文件或访问网络。

Host 与 Guest:import/export 的方向

从组件视角判断方向:

WIT 声明 组件含义 Host 要做什么
export api "我提供 api" 调用生成的 call_* 包装
import configuration "我需要配置" 在 Linker 中提供实现

在命令行或桌面应用中,用户输入、文件访问、应用配置和日志设施应由 Host 持有;Guest 仅获取明确且受限的 WIT capability 或已准备好的数据结构,而不应直接获取文件句柄或完整应用状态。

推荐的学习顺序

接下来两篇都围绕当前仓库,不要求另建 toy project:

  1. 第二篇:Rust Guest + Wasmtime Host:先走工具链最成熟的路径;
  2. 第三篇:TypeScript Guest 工程化:理解为什么 TS 需要生成声明,以及如何用 satisfies 防止接口漂移。

参考资料

相关推荐
红尘散仙1 小时前
从 WIT 到 Wasmtime:构建 Rust Component 工程
rust·typescript·webassembly
记忆张量MemTensor3 小时前
产品更新|MemOS 现已支持 DeepSeek Harness 长期记忆接入
人工智能·typescript·开源·agent
编码浪子7 小时前
Rust 智能指针深度实战:Box/Rc/Arc/RefCell 选型决策与内存管理避坑
开发语言·后端·rust
记忆张量MemTensor8 小时前
MemOS Skill 上线|一句话即可接入 MemOS Cloud
大数据·数据库·人工智能·typescript·开源
YIAN12 小时前
端侧大模型:DeepSeek-R1 WebGPU 推理全流程源码深度解析
前端·typescript·deepseek
苏灿烤鱼14 小时前
把求职写成工作流,公开 fork 为什么会把简历写进仓库?
python·typescript·claude
古夕1 天前
Vue 3 严格模式下,可选接口字段为什么会炸:一次上架页 TypeScript 复盘
typescript
对象存储与RustFS1 天前
用 rclone 把现有 S3/MinIO 数据同步到 RustFS
后端·rust·开源
EricStone1 天前
Agent开发学习一:Hello-Agents TypeScript 全栈实现
typescript·node.js·agent