第 16 章:扩展系统——Wasm 边界、贡献注册与长期兼容
一个编辑器扩展系统要同时解决“第三方能做什么”“宿主如何加载”“旧扩展能否继续运行”“不可信代码 如何隔离”。Zed 以 manifest + versioned WIT + Wasmtime host 划出边界。
1. 扩展并不直接拿到 Editor 对象
扩展通过 extension_api 实现公开 Extension trait,返回命令、配置、label 或 adapter 描述。宿主将这些 结果转换为语言、主题、debug adapter、context server 等内部 contribution。
extension.toml / languages / themes / grammars
+
extension.wasm
↓
extension_host
index / install / load / unload
↓
WasmHost + WIT
↓
LanguageRegistry / ThemeRegistry / DAP / Agent第三方代码看不到内部 Entity、GPUI window 或任意 Rust类型;公开 ABI 是唯一契约。
2. Extension trait 的能力面
crates/extension_api/src/extension_api.rs 的 Extension trait 提供可选方法,包括:
- language server command、initialization options、workspace configuration;
- completion/symbol label;
- slash command 执行与补全;
- context server command;
- docs package 索引;
- DAP adapter/locator 与 debug configuration。
默认实现返回 unsupported 或空值,所以新增可选能力不会强迫所有旧扩展立即升级。register_extension! 宏负责把扩展类型导出到 component ABI。
3. Manifest 与 Wasm 的职责不同
静态 manifest 声明 id、name、version、schema version、作者、repository 与 contributions;语言 query、 主题、icon 等数据文件无需执行代码就能索引。Wasm 只处理需要动态逻辑的部分,例如定位 language server binary 或把配置转换为启动参数。
好处是 extension gallery 和本地 index 可以在不运行第三方代码的情况下完成搜索、兼容性判断和资源发现。
4. ExtensionStore 是控制面
crates/extension_host/src/extension_host.rs 的 ExtensionStore 负责:
- 读取已安装 extension index;
- 安装、更新、卸载、reload 和 dev extension;
- 判断宿主/extension API 兼容性;
- load/unload Wasm instance;
- 将 contribution 注册到对应 registry;
- 发出 ExtensionsUpdated 等事件。
启动时优先同步读取缓存 index,保证编辑器尽快可用;若磁盘状态已经变化,再异步 rebuild 并发布新快照。 这是典型的“快缓存启动 + 后台校准”模式。
5. 为什么 load/unload 串行化
安装、卸载和 reload 会同时修改 index、Wasm instance map、language/theme registries。并行执行可能出现旧 unload 把新 load 注册的 contribution 删掉,或磁盘文件已经换代但内存仍指向旧实例。
ExtensionStore 使用一个串行 task loop 处理这些 operation(crates/extension_host/src/extension_host.rs: 401-420),把每个状态转换做成原子步骤。下载等昂贵 I/O 可在外部并行,最终提交必须排序。
6. WasmHost 的实例边界
crates/extension_host/src/wasm_host.rs 的 WasmHost 管理 Wasmtime engine、component linker、编译 cache 与宿主资源;每个 WasmExtension 保存 manifest、API version、component instance 和 state。
一次调用大致经过:
宿主 feature 请求
→ 查找 extension id + instance
→ 根据 API version 选择生成的 WIT bindings
→ 将宿主数据转换成 ABI record/list/string
→ Wasmtime 调用 guest export
→ 将结果转换回内部类型
→ 附加 extension id 作为错误上下文跨边界只传递 WIT 支持的值;大型内部对象用 resource handle 或提炼后的数据表达。
7. Versioned WIT 如何保住旧扩展
crates/extension_host/src/wasm_host/wit/ 保留多个 since_v0_* 世界,wit.rs 将 extension API version 映射到兼容 binding。宿主加载 manifest 后选择对应 adapter,而不是要求所有 .wasm 与最新 ABI 完全一致。
兼容层需要明确:
- 新增字段的默认值;
- enum 新 variant 对旧 guest 的处理;
- 方法从 unsupported 到 supported 的版本;
- 何时拒绝过旧 schema;
- host 内部类型变化如何隔离在转换层。
公开 ABI 一旦发布就是长期产品,不应直接暴露频繁变化的内部 struct。
8. WASI capability 不是完整主机权限
Wasm 默认不能任意访问本地文件、进程与网络。宿主针对明确场景提供 capability,例如:
- 下载 language server release;
- 执行受控 command;
- 查找/安装 npm package;
- 读取扩展自己的资源;
- 获取 worktree/config 的抽象信息。
每项能力都通过 host function 暴露,参数与结果可验证。扩展不应得到“整个编辑器进程权限”,否则 Wasm 隔离只剩格式转换意义。
9. Language server extension 的调用链
sequenceDiagram
participant L as LspStore
participant R as LanguageRegistry
participant E as ExtensionStore
participant W as WasmExtension
L->>R: 为 language 查找 adapter
R->>E: extension adapter 请求 command
E->>W: language_server_command(config, worktree)
W->>E: command + args + env
E->>E: 校验路径/capability
E-->>L: LanguageServerBinary
L->>L: 启动 server、initialize扩展只决定“如何得到和启动 server”,LSP 生命周期、document sync、diagnostics 与 restart 仍由 LspStore 统一管理。
10. 数据型 contribution 为什么更稳定
语法 query、language config、theme 等优先采用声明式文件。它们:
- 可在 gallery/build 阶段验证;
- 不必实例化 Wasm;
- 更容易缓存和热更新;
- 攻击面更小;
- 能被宿主批量组合。
只有声明式数据无法表达的行为才进入 Extension trait,减少 ABI 面积。
11. 编译 cache 与启动性能
首次把 component 编译成机器码较贵。WasmHost 使用受限的增量 compilation cache,以 extension 内容、 Wasmtime/host 版本等作为有效性条件。命中 cache 可快速实例化;失效时后台重新编译。
cache 必须有大小/淘汰上限,并把不可信 cache 数据视为可重建产物。不能因为追求启动速度而跳过版本校验。
12. 安装更新的事务边界
安全更新流程需要:
- 下载到临时位置并校验 package;
- 检查 manifest、schema 和 API compatibility;
- 原子替换已安装目录;
- 串行 unload 旧实例;
- rebuild/update index;
- load 新实例并注册 contribution;
- 失败时不留下半套文件或重复注册。
Dev extension 允许指向开发目录和 reload,但仍经过相同 manifest/Wasm 边界,避免开发模式形成另一套运行时。
13. Remote 模式中的扩展
语言服务器、debug adapter 等依赖 project 文件系统的能力应在 remote host 一侧运行;纯 UI/theme 能力可在 local。ExtensionStore 需要协调两端已安装版本和 contribution,不能假定所有 extension 只存在本地。
判断原则不是“扩展整体放哪”,而是“该 contribution 依赖哪个资源域”。同一扩展可以同时贡献本地主题 和远端 language server adapter。
14. 故障隔离
- Wasm trap:标记该 extension 调用失败,不使 host 进程崩溃;
- 超时/取消:长下载或 command 能终止;
- 无效返回值:在转换边界验证并附上 extension id;
- contribution 冲突:用 id/优先级决定,不依赖加载时序;
- API 不兼容:加载前拒绝并给出可升级信息;
- reload 中失败:清理部分注册,保留 Store 一致状态。
15. 可迁移经验
- 公开扩展 ABI 与内部对象模型彻底隔离;
- 静态 contribution 优先,动态代码最小化;
- 版本化 schema/WIT 是产品承诺,不是构建细节;
- cache 可以过期,index 与 load 状态必须可重建;
- 下载并行,最终 load/unload 状态转换串行;
- capability 逐项授权,不把完整主机能力塞进 Wasm;
- extension failure 带身份上下文,并限制在单个 contribution 内。