← 返回博客

Linux 何时发布?

2024 年 5 月 7 日


看看这个

Screenshot of Zed — but where are the red/yellow/green window controls?
Zed 的屏幕截图 — 但红色/黄色/绿色窗口控件在哪里?

有什么突出的地方吗?是的,没错,这是 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_closeon_fullscreen)、属性(is_fullscreenis_minimized)、基本功能(content_sizemouse_positiontoggle_fullscreen)和棘手的功能(scale_factorappearance)。

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 的全部内容,但是:哇。

Screenshot of kvark's pull request that made Zed render on Linux
kvark 的 pull request 的截图,该请求使 Zed 可以在 Linux 上渲染

这个 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());
    }
 
    // [...]
}

PlatformLinuxClient 上实现,而它本身也是一个 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
  • 然后它调用 PlatformWindowdraw 这个 Scene
  • Wayland 和 X11 的实现都使用 Blade,而 Blade 使用 Vulkan 与 GPU 通信,以绘制 Scene

这显然只是皮毛。Linux 实现中还有很多更有趣的事情发生——你知道 Linux 上有多个剪贴板,但 macOS 上只有一个吗?或者,看看这个

Screenshot of Zed on Linux, including a native file dialog
Linux 上 Zed 的截图,包括一个原生文件对话框

是的,这是一个原生文件对话框,但我们在 Zed 中没有使用 GTK,而这显然是 GTK——那么,它是如何工作的?你会在 配套视频 中找到答案,在视频中 Mikayla 和我更深入地探讨了这个问题,并且还涉及了我们是否应该使用 GTK 或 Qt 或其他东西的问题(剧透:这很复杂)。

那么,Linux 版何时发布?

那么 Linux 还需要做些什么?为了达到 alpha 发布:*没有那么多*,但不要引用我的话。本质上:修复剩余的 86 个不同难度级别的 todo!,使窗口大小调整/移动在 Wayland 上工作,并为 GPUI 实现系统对话框。我们很接近了,非常接近。

在 alpha 发布之后,我们需要添加对音频通话、拖放、凭据存储的支持,确保性能始终如一地高,提高稳定性等等。查看 Linux 路线图跟踪问题。

即使可能还有很多事情要做(我甚至不想知道在 Linux 上拖放有多复杂——我的意思是,GNOM——我的意思是,KDE,不,我的意思是——)以及很多未知的未知数和惊喜,但有一件事是肯定的:我们能在三个月内使 Linux 上的 Zed 达到当前状态,并且有如此多的高质量开源贡献,这真是太棒了。

想在 Linux 上试用 Zed 吗?你需要 Rust、一些依赖项,并且取决于你的耐心,需要足够的 CPU 和内存才能在合理的时间内编译它。看看这些说明。玩得开心!