← 返回博客

Linux 何时推出?

看这里

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

有什么特别之处吗?是的,没错,这是 Zed 在 Linux 上运行的截图!

等等,什么?Zed 在 Linux 上?发布了吗?没有,还没发布,但正在快速成形。

一月底,我们将 Zed 开源,当时完全不支持 Linux。现在,三个月后,你可以在 Linux 上编译和运行 Zed,并且实际使用它。我是说 真正地 使用它——我上周一整周都在 Zed 中(在 Zed 上!)工作,没有任何问题。

“alpha 版本”这个词似乎出现在隧道的尽头。我们越来越近了。

今天我们将讨论 Zed 在 Linux 上是如何成形的,遇到了哪些挑战,谁完成了这些工作,以及还有哪些工作要做。

伴随视频

Linux 何时推出?

这篇博文附带一个时长1小时的配套视频,其中 Thorsten 和 Mikayla 探索了 Linux 上的 Zed,深入研究了代码库以了解其实现方式,讨论了实现挑战,以及开源社区是如何提供了巨大帮助的。

在此观看视频 →

Linux when?

为什么不从一开始就支持 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!() 而崩溃。早在二月份,这个文件有 11 个 todo! 注释和 9 次 unimplemented 调用。

现在它有 9 个 todo!(不同的)和字符串中的 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 在 获得用户,但 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)带着现在传奇般的 PR 出现了,它让 Zed 可以在 Linux 上渲染。代码不到四千行。当然,它还没有渲染 Zed 的所有部分,但是:哇。

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

这个 PR 回答了如何在 Linux 上与 GPU 通信的问题:使用 blade,kvark 的“Rust 渲染解决方案”,它提供了“精简的低级 GPU 抽象,专注于人体工程学和乐趣”。Blade 在底层使用 Vulkan,一个在抽象级别上与 Metal 类似的图形 API,用于与 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 上实现,而 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
  • 然后它调用 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 上的拖放有多复杂——我的意思是,GNOME——我的意思是,KDE,不,我的意思——)以及一路上会有许多未知因素和惊喜,但有一点是肯定的:我们能够在三个月内将 Zed 在 Linux 上的状态达到目前这样,并且有这么多高质量的开源贡献,这真是太棒了。

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


正在寻找更好的编辑器吗?

您今天就可以在 macOS、Windows 或 Linux 上试用 Zed。立即下载


我们正在招聘!

如果您对我们博客中涵盖的主题充满热情,请考虑加入我们的团队,帮助我们实现软件开发的未来。