Skip to content

第 16 章:扩展系统——Wasm 边界、贡献注册与长期兼容

一个编辑器扩展系统要同时解决“第三方能做什么”“宿主如何加载”“旧扩展能否继续运行”“不可信代码 如何隔离”。Zed 以 manifest + versioned WIT + Wasmtime host 划出边界。

1. 扩展并不直接拿到 Editor 对象

扩展通过 extension_api 实现公开 Extension trait,返回命令、配置、label 或 adapter 描述。宿主将这些 结果转换为语言、主题、debug adapter、context server 等内部 contribution。

text
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.rsExtension 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.rsExtensionStore 负责:

  • 读取已安装 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.rsWasmHost 管理 Wasmtime engine、component linker、编译 cache 与宿主资源;每个 WasmExtension 保存 manifest、API version、component instance 和 state。

一次调用大致经过:

text
宿主 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 的调用链

mermaid
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. 安装更新的事务边界

安全更新流程需要:

  1. 下载到临时位置并校验 package;
  2. 检查 manifest、schema 和 API compatibility;
  3. 原子替换已安装目录;
  4. 串行 unload 旧实例;
  5. rebuild/update index;
  6. load 新实例并注册 contribution;
  7. 失败时不留下半套文件或重复注册。

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. 可迁移经验

  1. 公开扩展 ABI 与内部对象模型彻底隔离;
  2. 静态 contribution 优先,动态代码最小化;
  3. 版本化 schema/WIT 是产品承诺,不是构建细节;
  4. cache 可以过期,index 与 load 状态必须可重建;
  5. 下载并行,最终 load/unload 状态转换串行;
  6. capability 逐项授权,不把完整主机能力塞进 Wasm;
  7. extension failure 带身份上下文,并限制在单个 contribution 内。

独立源码学习笔记 · 文档采用 CC BY-SA 4.0