看看这个

有什么突出的地方吗?是的,没错,这是 Zed 运行在 Linux 上的屏幕截图!
等等,什么?Zed 在 Linux 上?已经发布了吗?不,还没有,但它正在快速成形。
在一月底,我们将 Zed 开源,并且没有任何 Linux 支持。现在,三个月后,您可以编译并运行 Zed 在 Linux 上,并且可以真正使用它。我的意思是真正使用它 — 上周我一直在 Zed (在 Zed 上!) 工作,没有任何问题。
“Alpha 发布”这个词似乎出现在隧道的尽头。我们越来越近了。
今天,我们将讨论 Zed 在 Linux 上是如何成形的,面临哪些挑战,谁完成了这项工作,以及还剩下什么需要完成。
配套视频
Linux 何时发布?
这篇文章附带一个 1 小时的配套视频,其中 Thorsten 和 Mikayla 探索了 Zed 在 Linux 上的运行情况,深入研究了代码库以了解其实现方式,讨论了实现挑战,以及开源社区如何提供了巨大的帮助。
为什么一开始没有 Linux 版本?或者:关于平台的棘手之处
为什么 Zed 不能直接在 Linux 上运行?问这个问题并不过分,而且你们中的许多人都问了。毕竟,Zed 是用 Rust 编写的,Rust 是一种以其跨平台支持而闻名的语言。因此,如果 Rust 程序可以在 macOS、Linux 和 Windows 上运行,为什么 Zed 不能?
跨平台支持的棘手之处在于 — 一般来说,在 Rust 中 — 只要您对平台被抽象出来,隐藏在每个平台上都相同的 API 后面,它才能工作。
然而,在 Zed,我们希望尽可能地利用每个平台来构建一个高性能的应用程序,它对平台来说是原生的,并且感觉是原生的。这通常意味着直接与平台对话,以便充分利用它的能力。
例如,在 macOS 上,Zed 直接使用Metal。我们有我们自己的着色器,我们的自己的渲染器,并且我们付出了大量努力来理解 macOS API 以达到 120FPS。macOS 上的 Zed 也是一个完全原生的 AppKit NSApplication,并且我们将我们的异步 Rust 运行时与 macOS 的原生应用程序运行时集成。
如果你希望你的应用程序对平台的集成有这种程度的深度和控制,并且希望它是跨平台的,那么你需要构建的是一个框架。一个框架,允许你在需要时直接与平台对话,否则将它抽象出来,这样你就不必在编写应用程序级代码时担心它。
这就是 Zed 所做的。该框架称为 GPUI,当我们将其与 Zed 一起开源时,它带有跨平台支持。除了实际上不是。
GPUI 的跨平台支持
GPUI 确实抽象了底层平台,并假设世界上不仅仅只有 macOS。以下是 GPUI 的 Platform
trait 的一部分,这是一个 GPUI 应用程序必须与之交互的所有内容
// crates/gpui/src/platform.rs
trait Platform: 'static {
// [... some methods left out to keep this short ...]
fn background_executor(&self) -> BackgroundExecutor;
fn foreground_executor(&self) -> ForegroundExecutor;
fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
fn quit(&self);
fn restart(&self);
fn hide(&self);
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
fn open_window(
&self,
handle: AnyWindowHandle,
options: WindowParams,
) -> Box<dyn PlatformWindow>;
fn window_appearance(&self) -> WindowAppearance;
fn open_url(&self, url: &str);
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
fn prompt_for_paths(&self, options: PathPromptOptions, ) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
fn reveal_path(&self, path: &Path);
// [...]
fn local_timezone(&self) -> UtcOffset;
fn set_cursor_style(&self, style: CursorStyle);
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
}
这个 Platform
提供了应用程序可能想要做的几乎所有事情:在不同的线程上执行工作,启动/停止应用程序,管理窗口,打开 URL,打开系统对话框以打开文件和目录,光标样式,剪贴板,凭据,... 以及我省略的更多内容,以保持简洁。
从一开始,GPUI 就内置了这种 Platform
抽象。GPUI 从来都不是仅限 macOS 的。只要为平台实现了 Platform
trait,它就始终是平台无关的。
这本质上就是让 Zed 在 Linux 上运行的意义:为 Linux 实现 Platform
。
所以,只是实现了一堆方法,然后 Zed 就可以在任何平台上运行了?好吧... 某种程度上是这样的。
这是二月中旬 Linux Platform
实现的样子。再次,摘录
// crates/gpui/src/platform/linux/platform.rs @ 266988adea
impl Platform for LinuxPlatform {
// [... some methods left out to keep this short ...]
fn background_executor(&self) -> BackgroundExecutor {
self.inner.background_executor.clone()
}
fn foreground_executor(&self) -> ForegroundExecutor {
self.inner.foreground_executor.clone()
}
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
self.inner.text_system.clone()
}
fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
self.client.run(on_finish_launching)
}
fn quit(&self) {
self.inner.state.lock().quit_requested = true;
}
//todo!(linux)
fn restart(&self) {}
//todo!(linux)
fn hide(&self) {}
//todo!(linux)
fn unhide_other_apps(&self) {}
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
unimplemented!()
}
fn reveal_path(&self, path: &Path) {
unimplemented!()
}
//todo!(linux)
fn write_to_clipboard(&self, item: ClipboardItem) {}
//todo!(linux)
fn read_from_clipboard(&self) -> Option<ClipboardItem> { None }
}
一些方法已实现,一些标记为 todo!
,另一些因 unimplemented!()
而 panic。二月份,此文件有 11 个 todo!
注释和 9 个对 unimplemented
的调用。
现在 它有 9 个 todo!
s(不同的)和字符串中的单词 unimplemented
。
所以,是的,这项工作的重点是移除那些 todo!
并实现缺失的方法。但我们都知道:并非所有方法都一样重要。
虽然 write_to_clipboard
方法的实现看起来相对简单,但实际情况并非像小写的 todo!
及其可爱的感叹号所暗示的那样容易。
你说的Linux是哪个?
为 Linux 构建 GUI 应用程序的一大挑战是,实际上并不存在一个统一的 Linux。Linux 只是一个内核,当你安装并运行它时,你很可能通过 Linux 发行版来完成,这些发行版也为你提供了操作系统的其余部分:Ubuntu、Debian、CentOS、Arch、Gentoo 等。
它们几乎都在应用程序开发人员相关的某些方面有所不同。例如:软件包管理。发行版不仅有自己的应用程序分发格式,而且还有不同的依赖项管理方式。而且发行版之间没有标准(但当然有相互竞争的标准)。到目前为止,对于 Zed,我们避免了整个打包问题,只专注于构建 .tar.gz
压缩包,然后由其他人将其转换为特定于发行版的软件包。但是,仅仅浏览 Tailscale 的软件包站点 就告诉我们,有很多工作要做,而这些工作是我们以前在 macOS 上从未做过的。
但即使你只关注一个发行版,比如 Ubuntu,你仍然需要决定:你支持 X11 还是 Wayland?忽略一些技术细节(不要给我发愤怒的信),两者都是显示服务器,Linux 软件可以通过它们在屏幕上绘制内容。X11 已经存在很长时间了,而 Wayland 一直试图取代它,也已经很长时间了。反过来,这使得这个问题成了一个棘手的问题:你必须同时支持两者。它们都被广泛使用。就 X11 而言,它 *仍然在使用*,而 Wayland 的 *用户正在增加*,但认为 100% 的 Linux 用户很快就会使用 Wayland 并不现实。
在决定支持 Ubuntu、X11 和 Wayland 之后,下一个问题是:你要支持哪个桌面环境或窗口管理器?KDE 还是 GNOME?Qt 还是 GTK?平铺窗口管理器和希望关闭窗口装饰的用户呢?你要支持哪个*音频服务器*才能使音频通话正常工作?PipeWire?还是仍然是 PulseAudio?你甚至需要担心这些吗,还是说这些选择由你选择的桌面环境决定?
我可能还可以继续找到更多“实际上,X 不止一个”的例子来列举,但你明白了:在构建在 Linux 上运行的图形应用程序时,你必须就目标平台组合和方式做出相当多的技术决策。
我们甚至还没有触及所有这些决策中最基本的一个:我们如何渲染 Zed?
从 Metal 到...什么?
即使你知道如何打包你的应用程序,以及如何以 X11 和/或 Wayland 为目标,并设法在其中打开一个应用程序窗口 - 你如何在那个窗口中绘制你的应用程序?
你可能错过了,但在上面的 Platform
trait 中,有一个打开新窗口的方法
// crates/gpui/src/platform.rs
trait Platform: 'static {
// [...]
fn open_window(
&self,
handle: AnyWindowHandle,
options: WindowParams,
) -> Box<dyn PlatformWindow>;
}
为了实现这一点,你还必须实现它返回的东西的接口:PlatformWindow
。这可不是一件容易的事。
到目前为止,PlatformWindow
需要实现 37 种方法。这些方法包括钩子(on_close
、on_fullscreen
)、属性(is_fullscreen
、is_minimized
)、基本功能(content_size
、mouse_position
、toggle_fullscreen
)和棘手的功能(scale_factor
、appearance
)。
但 PlatformWindow
上有一个方法是最根本的:draw
。它的另一个名字可能是 where_the_rubber_hits_the_road
。这是它在 macOS 上的实现
// crates/gpui/src/platform/mac/window.rs
impl PlatformWindow for MacWindow {
// [...]
fn draw(&self, scene: &Scene) {
let mut this = self.0.lock();
this.renderer.draw(scene);
}
}
它看起来并不起眼,但实际上很重要:Scene
是 GPUI 渲染一帧的结果,而这个 draw
方法使得它出现在屏幕上。
Scene
是应用程序的可见部分——文本、窗口、边框、矩形、下划线等等——被转换为数据,也就是*图元*。这是 Scene
的定义
// crates/gpui/src/scene.rs, simplified
struct Scene {
shadows: Vec<Shadow>,
quads: Vec<Quad>,
paths: Vec<Path<ScaledPixels>>,
underlines: Vec<Underline>,
monochrome_sprites: Vec<MonochromeSprite>,
polychrome_sprites: Vec<PolychromeSprite>,
surfaces: Vec<Surface>,
paint_operations: Vec<PaintOperation>,
primitive_bounds: BoundsTree<ScaledPixels>,
layer_stack: Vec<DrawOrder>,
}
阴影、四边形、路径、下划线、精灵 - 这就是 GPUI 在渲染一帧时将您的应用程序分解成的最终形态。然后在 draw
中,它将该数据交给渲染器。
渲染器——那就是你的 GPU。
正如我上面提到的:在 macOS 上,我们使用 Metal API 与 GPU 通信,当你遵循这些定义时,你最终会得到 一个在 MetalRenderer
上实现的 fn draw
,其中包含了我们 120FPS 博客文章中讲述的几乎完整的故事:设置渲染管线、三级缓冲、与操作系统和显示器同步,所有这些。
那么,我们如何在没有 Metal 的情况下在 Linux 上实现 draw
?我们如何在 Linux 上与 GPU 通信?
开源黑客登场
在 Zed 开源不到两周后,@kvark (Dzmitry Malyshau) 从所有兴奋引起的 pull request 和 issue 的云团中脱颖而出,带来了一个现在已经传奇的 PR,使得 Zed 可以在 Linux 上渲染。只用了不到四千行的代码。当然,它还没有渲染 Zed 的全部内容,但是:哇。

这个 PR 回答了如何在 Linux 上与 GPU 通信的问题:使用 blade,kvark 的“Rust 渲染解决方案”,它提供了一个“精简的底层 GPU 抽象,专注于人体工程学和乐趣”。Blade 在底层使用 Vulkan,这是一种图形 API,在它提供的抽象级别上与 Metal 类似,用于与 GPU 通信并渲染 Zed。
在 @kvark 打开 PR 之后,Mikayla、Antonio 和 Nathan 联系了他,讨论在 Zed 中使用 Blade 的可行性和共同目标。他们很快就达成了一致,并且在 Zed 开源两周后,使它能够在 Linux 上编译和运行的 PR 被合并了。
随着这个重要的 TODO 和问题解决后,社区抓住机会移除了剩余的 todo!
:@witelokk 添加了对 Wayland 的支持,@kvark 继续了他的工作,使整个 Zed 能够流畅渲染,@romgrk 添加了对文件对话框的支持,@apricobucket28 修复了滚动和选择问题,以及许多其他人 贡献并修复了很多其他东西,而 Mikayla 则兢兢业业地进行审查、编码、管理、领导并关注一切。
这个贡献列表是不完整的,而且我确信我应该提到更多的人,但我想表达的观点是:Linux 上的 Zed 是一个令人印象深刻的开源团队协作成果,它以如此快的速度将 Zed 带到了一个可工作的状态,这让我们 Zed 的所有人都感到惊讶。
Linux 上的 Zed:抽象
让我们看看它是如何工作的,X11、Wayland 和 Blade 是如何隐藏在 Platform
抽象之下的。
这是一个摘录
// crates/gpui/src/platform/linux/platform.rs
impl<P: LinuxClient + 'static> Platform for P {
fn background_executor(&self) -> BackgroundExecutor {
self.with_common(|common| common.background_executor.clone())
}
fn foreground_executor(&self) -> ForegroundExecutor {
self.with_common(|common| common.foreground_executor.clone())
}
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
self.with_common(|common| common.text_system.clone())
}
fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
on_finish_launching();
LinuxClient::run(self);
self.with_common(|common| {
if let Some(mut fun) = common.callbacks.quit.take() {
fun();
}
});
}
fn quit(&self) {
self.with_common(|common| common.signal.stop());
}
// [...]
}
Platform
在 LinuxClient
上实现,而它本身也是一个 trait
// crates/gpui/src/platform/linux/platform.rs
trait LinuxClient {
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
fn open_window(
&self,
handle: AnyWindowHandle,
options: WindowParams,
) -> Box<dyn PlatformWindow>;
fn set_cursor_style(&self, style: CursorStyle);
fn write_to_primary(&self, item: ClipboardItem);
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_primary(&self) -> Option<ClipboardItem>;
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn run(&self);
}
并且有 LinuxClient
的两个实现
它们都有一个 PlatformWindow
的实现
但它们都在它们的 draw
方法中使用 Blade
// crates/gpui/src/platform/linux/x11/window.rs
struct X11WindowState {
// [... other fields ...]
renderer: BladeRenderer
}
impl PlatformWindow for X11Window {
// [...]
fn draw(&self, scene: &Scene) {
let mut inner = self.0.state.borrow_mut();
inner.renderer.draw(scene);
}
}
// crates/gpui/src/platform/linux/wayland/window.rs
struct WaylandWindowState {
// [... other fields ...]
renderer: BladeRenderer,
}
impl PlatformWindow for WaylandWindow {
fn draw(&self, scene: &Scene) {
let mut state = self.borrow_mut();
state.renderer.draw(scene);
}
}
这意味着真正的实现是通过这些抽象层实现的
- GPUI 要求
Platform
打开一个窗口 - Wayland 的
LinuxClient
使用 Wayland 打开一个窗口,X11 客户端使用 X11 - 在渲染一个帧时,GPUI 将应用程序转换为一个
Scene
- 然后它调用
PlatformWindow
来draw
这个Scene
- Wayland 和 X11 的实现都使用 Blade,而 Blade 使用 Vulkan 与 GPU 通信,以绘制
Scene
这显然只是皮毛。Linux 实现中还有很多更有趣的事情发生——你知道 Linux 上有多个剪贴板,但 macOS 上只有一个吗?或者,看看这个

是的,这是一个原生文件对话框,但我们在 Zed 中没有使用 GTK,而这显然是 GTK——那么,它是如何工作的?你会在 配套视频 中找到答案,在视频中 Mikayla 和我更深入地探讨了这个问题,并且还涉及了我们是否应该使用 GTK 或 Qt 或其他东西的问题(剧透:这很复杂)。
那么,Linux 版何时发布?
那么 Linux 还需要做些什么?为了达到 alpha 发布:*没有那么多*,但不要引用我的话。本质上:修复剩余的 86 个不同难度级别的 todo!
,使窗口大小调整/移动在 Wayland 上工作,并为 GPUI 实现系统对话框。我们很接近了,非常接近。
在 alpha 发布之后,我们需要添加对音频通话、拖放、凭据存储的支持,确保性能始终如一地高,提高稳定性等等。查看 Linux 路线图跟踪问题。
即使可能还有很多事情要做(我甚至不想知道在 Linux 上拖放有多复杂——我的意思是,GNOM——我的意思是,KDE,不,我的意思是——)以及很多未知的未知数和惊喜,但有一件事是肯定的:我们能在三个月内使 Linux 上的 Zed 达到当前状态,并且有如此多的高质量开源贡献,这真是太棒了。
想在 Linux 上试用 Zed 吗?你需要 Rust、一些依赖项,并且取决于你的耐心,需要足够的 CPU 和内存才能在合理的时间内编译它。看看这些说明。玩得开心!