Skip to content

第 3 章:启动与组合根——一个产品如何被装配出来

crates/zed/src/main.rs 的核心职责不是实现功能,而是决定进程模式、建立基础设施、按依赖 顺序注册能力,最后恢复或创建 Workspace。它是整套应用的 composition root。

1. 一个二进制,多种进程角色

main() 进入 GUI 前先检查特殊模式:

参数/条件进程角色为什么必须最先处理
sandbox launcherLinux 沙箱 helper后续参数属于被包装命令,不能被 Zed CLI 解析
--askpassGit/SSH askpass bridge只做 socket I/O,不应启动 UI
--crash-handlerminidump server独立监控主进程
ETW trace 参数Windows trace recorder只负责采样与输出
--printenvshell env probe被 CLI/安装逻辑调用
--dump-all-actions开发工具输出 action catalog 后退出

证据:crates/zed/src/main.rs:200-269

这个设计比为每个 helper 单独发一个二进制更方便打包,但要求分支尽早、依赖尽量少。

2. 启动分成六个阶段

text
阶段 1 进程分流与路径准备

阶段 2 日志、trace、版本、线程池

阶段 3 Application、DB、Session、单实例、Crash handler

阶段 4 Fs / Settings / HTTP / Client / LanguageRegistry

阶段 5 init 注册产品能力,构建 AppState

阶段 6 恢复 session、处理 open request、创建 Workspace

阶段 1:先保证最小可运行环境

init_paths() 创建配置、日志、扩展、数据库等目录。如果失败,Zed 仍尝试建立最小 GPUI Application,显示 critical prompt;连窗口也打不开才退到 stderr/系统通知。

这条失败路径很重要:错误 UI 不能依赖尚未成功初始化的 Settings、Theme 或 Workspace。

源码:crates/zed/src/main.rs:95-197:1654-1678

阶段 2:日志和并行计算先于产品对象

Zed 根据 stdout 是否为 TTY 选择日志文件或 stdout,然后初始化 tracing。全局 Rayon pool 使用 大约一半可用 CPU,并把 worker stack 设为 10 MiB。

源码:crates/zed/src/main.rs:293-328

这里可以看出两类异步并存:

  • GPUI executor/Tokio 处理有生命周期的 async I/O;
  • Rayon 处理 CPU 密集并行计算。

阶段 3:先创建不会依赖 Workspace 的基础设施

build_application() 选择当前 GPUI platform,并根据实验环境变量决定是否开启 accessibility。 接着创建:

  • db::AppDatabase
  • system/installation/session id 的后台任务;
  • OpenListener
  • 单实例监听;
  • crash handler;
  • Git provider registry 与 RealFs
  • keymap/config watcher;
  • login shell environment 加载任务。

源码:crates/zed/src/main.rs:343-453

阶段 4:进入 app.run,建立 App 全局世界

app.run(move |cx| { ... }) 是关键边界。闭包之前可以准备线程安全的 service;闭包内部拿到 &mut App,才能创建 Entity、设置 Global、注册 Action 和观察者。

最先注册的能力包括:

text
AppDatabase
  → TrustedWorktrees
  → menu / zed_actions
  → ReleaseChannel / gpui_tokio
  → SettingsStore + watcher
  → HttpClient / Fs / GitHostingRegistry
  → Extension proxy
  → Client / LanguageRegistry / NodeRuntime

源码:crates/zed/src/main.rs:478-565

顺序不是装饰。比如 languages::init 需要 LanguageRegistry、Fs 和 NodeRuntime; language_extension::init 又需要 ExtensionHostProxy 和 LanguageRegistry。

3. init(cx) 模式到底做什么

Zed 中 foo::init(cx) 通常不是构造一个大单例,而是做下列一项或多项:

  1. 注册 Action handler;
  2. 注册 Settings 类型;
  3. 把某个 registry 放进 Global;
  4. 注册 ProjectItem/Panel/Toolbar;
  5. 订阅全局状态变化;
  6. 把协议 handler 挂到 Client/RemoteClient;
  7. 注册语言、provider、debug adapter 或 extension proxy。

例如 zed::init(cx) 注册应用级 Hide/Quit/打开设置/打开任务等 Action,并观察 feature flag。 源码:crates/zed/src/zed.rs:191-300

为什么不用一个自动依赖注入容器

显式 init 序列有三点优势:

  • 编译器检查函数签名中的依赖;
  • 启动顺序在一个文件里可审计;
  • 平台和 feature 分支可以使用普通 Rust 条件编译。

代价是 main.rs 很长,而且遗漏某个 init 往往表现为“Action 不工作/类型没注册”,而非构造 阶段立刻报错。

4. AppState 是产品级依赖包

经过 Client、Fs、LanguageRegistry、NodeRuntime、Session 等准备后,启动代码构造 workspace::AppState。Workspace 和大量 UI 模块共享它,而不是各自从 Global 抓取全部依赖。

AppState 的典型成员包括:

  • client / user store;
  • language registry;
  • fs;
  • workspace store;
  • session;
  • node runtime;
  • build/release 元数据。

AppState::set_global 同时保留了从任意应用上下文获取产品状态的入口。源码位置: crates/workspace/src/workspace.rs:1122-1193

5. 为什么 Editor 的 init 靠后

主启动序列大致先初始化模型/Agent,再初始化 Editor、Workspace 和面板:

text
language_model / language_models
  → agent_ui
  → repl / recent_projects / dev_container
  → editor
  → diagnostics
  → workspace
  → file_finder / project_panel / outline_panel / tasks_ui
  → vim / terminal_view / collab_ui / git_ui / previews / settings_ui

源码:crates/zed/src/main.rs:683-785

原因不是 AI 比 Editor 更底层,而是许多 init 只是注册 provider 或类型;Workspace init 之后, 具体面板才能把自己挂到窗口/项目模型上。阅读 init 顺序时必须区分“注册定义”与“创建实例”。

6. 启动不是等所有工作完成

Zed 在 UI 可用前不会同步等待所有慢任务:

  • system id、installation id、session 异步打开;
  • login shell environment 后台加载;
  • extension index 必要时异步重建;
  • theme/language 目录 watcher 持续运行;
  • 自动更新、认证、遥测在后台推进;
  • worktree 初始扫描提供可等待 future,但 Workspace 可先显示。

这是一种“先建立最小一致状态,再增量补齐能力”的启动策略。

7. OpenListener 统一所有“打开”入口

文件可以从多条路径要求 Zed 打开:

  • CLI 连接已有实例;
  • OS open URL
  • zed:// 深链;
  • 首次启动参数;
  • diff path;
  • remote connection request。

OpenListener 把它们归一成 RawOpenRequest / OpenRequestapp.on_open_urls 只负责投递, 后续 handle_open_request 再解析路径位置、remote options、workspace 复用策略。

源码:crates/zed/src/main.rs:357-378:455-476:1000-1363

8. Session 恢复的边界

启动结尾必须在三种结果中选择:

  1. 明确 open request:按请求打开目标;
  2. 可恢复 session:重建 workspace、pane、item 和最近路径;
  3. 没有历史状态:创建新 workspace 或 onboarding。

恢复逻辑不会序列化任意 Rust 对象,而是通过 Workspace/Item registry 把持久化 model 转回 具体 item。这就是为什么可打开 item 要注册序列化/反序列化描述符。

9. 失败与取消路径

启动代码特别值得学习的不是 happy path,而是降级:

故障行为
目录不可创建最小 GPUI critical prompt
日志文件失败回退 stdout
已有实例投递 open request 或退出
crash handler 不适用强制 backtrace
shell env 尚未加载通过 oneshot 在需要处等待
session 数据损坏记录错误,创建新 Workspace
窗口创建失败stderr + 平台通知/退出

“启动成功”不是一个布尔值,而是一组能力逐步变为 available 的过程。

10. 可迁移的组合根模式

从 Zed 启动代码可以带走五条实践:

  1. helper 模式在解析主应用参数前分流;
  2. 错误展示路径只依赖最小基础设施;
  3. 在线程安全阶段准备 service,在 App 上下文中创建 UI entity;
  4. 用显式 init 顺序暴露依赖,而不是把装配分散到静态初始化;
  5. 慢能力异步补齐,但为依赖方提供明确的 wait/observe 接口。

下一章进入所有这些 cxEntityTask 背后的运行时:GPUI。

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