演讲:介绍 Ghostty 及几个实用的 Zig 模式
原文由 Mitchell Hashimoto 于 发布,订阅该博客
这是我在 Zig Showtime 上所做演讲的文字版。如果你更喜欢看视频,可以在 YouTube 上观看:Zig Showtime: Ghostty。视频末尾还包含问答环节,本文未予收录。

大家好!很高兴今天能来聊聊 Ghostty。Ghostty 是一款用 Zig 从零开始编写的全新终端模拟器。
说明:在撰写本文时,Ghostty 尚未公开发布。我会在演讲中多次提到这一点,但想先在此说明,以免大家一开始产生误解。计划是在 2024 年的某个时候以免费开源的形式发布 Ghostty。目前我们正在进行封闭测试(提供源码访问权限)。我在演讲的多个环节也会谈到这一点。

我叫 Mitchell!我就是那种特别、特别、特别热爱编程的人。
过去我发起过一些比较知名的软件项目,也许你听说过,也许没有:Vagrant、Terraform、Vault 等等。不过,我已经很多年没有参与这些项目了。
我也是那种永远都在做副业项目的人。永远如此。大约十年前,我写过一款《炉石传说》日志分析和回放软件,并把它卖给了一支职业电竞战队。几年前,我又从零写了一个邮件服务器,用来为我的一些域名提供邮件服务。而现在,嗯,现在我做了一个终端模拟器。
这张幻灯片是临近晚饭时做的,当时我特别饿,所以干脆放了一堆我吃东西的照片。我并不是对美食特别狂热之类的,只是此刻……真的很饿。而且我喜欢那种能把整个房间氛围串起来的主题元素。

不过这次演讲的主角是 Ghostty!这款终端模拟器!
在深入之前,先来看看 Ghostty 的样子!仅从这一张截图,你就能看出 Ghostty 功能相当丰富!
Ghostty 是 macOS 和 Linux 上的原生应用。它支持分屏、真彩色,能很好地运行 vim,支持多种字体样式(粗体、斜体等),支持 Kitty 图形协议等等。而这仅仅是在这张截图里能看到的!

如果你还不知道什么是终端模拟器,其实你很可能已经用过,这里列举了几款。实际上还有几十上百款。

既然已经有这么多,为什么还要再做一个?

从根本上说,在我看来,这就是当今终端模拟器的现状。你有速度快的终端、功能丰富的终端和原生的终端。你最多只能同时拥有其中两项。

而 Ghostty 的目标是——在我看来也已经实现了——兼具所有特性。

快速、功能丰富、原生体验并非互斥。
我认为 Ghostty 如今的表现已经足以配得上一个绿色对勾,而且我们还有很多可以做得更好的地方。
另外,提前说明一下,我对于原生平台体验的标准是:应用在观感和行为上都像一个为该平台量身打造的原生应用。我认为这个标准并不过分。例如,说 Alacritty 不算完全原生,因为新建窗口会创建新进程,我觉得这并不苛刻。或者说 Kitty 不算完全原生,因为它的标签页使用了非原生控件,也是如此(每款终端都还有很多类似的例子)。

所以在“基础”层面,我想做到的是:拥有 Alacritty 的速度、Kitty 的功能丰富度、以及 iTerm(macOS 上)的原生集成度,等等。

但我想更进一步。我希望终端能成为文本应用开发的现代平台,就像浏览器之于图形界面应用开发的现代平台一样(无论好坏)。
浏览器每年都会推出数十个(甚至上百个?)新特性。它们快速创新,让应用开发者感到兴奋。这么说可能有点夸张,但我认为终端本可以比现在创新得更快。
这里列举了一些例子:
- 进度条!为什么我们每次都要(而且大多数时候画得很糟糕地)重绘进度条?
- 拖放功能,让编辑器和其他应用可以表现得更原生。
- 标签页/分屏控制,让复用器能够利用现代终端模拟器的性能和特性。
- 鼠标手势,让 TUI 能够响应轻扫、多点触控、惯性滑动等操作。
- 安全特性:如今终端转义序列是个令人担忧的地方。以后再详谈。我们可以做得更好。
- 还有更多!

Ghostty 快吗?我们怎么知道?衡量终端模拟器的方法有很多,我不想在这次演讲中花大量时间讨论这个。我想表达的是,Ghostty 是“快的”。我不会声称它是“最快的”。
这里我展示了 Ghostty 运行一个会记录 FPS 的 DOOM 火焰动画。这是一个很好的压力测试,因为它会修改大量单元格,同时还会进行回滚。
可以看到 Ghostty 保持在 480 到 500 FPS 左右,动画非常非常流畅。相比之下,macOS 官方的 Terminal.app(为了不针对任何优秀的社区项目)则非常卡顿,不到 10 FPS。如果你使用 Alacritty 或 Kitty 这类“快速”终端模拟器,会看到与 Ghostty 相近的数字。
重点是:我并不是想说 Ghostty 是最快的,但它属于“快速”这一档,所以我认为表格里的绿色对勾是当之无愧的。
你也可以在自己的终端上试试这个,看看效果如何:https://github.com/const-void/DOOM-fire-zig/
文字版说明:幻灯片图片为静态图,无法展示视频。Ghostty 运行上述链接中的程序时帧率约为 480 到 500 FPS。

Ghostty 功能丰富吗?我不知道该如何量化,但这里有一份我们支持的功能的定性列表,其中很多都相对少见。

在 macOS 上,主要的图形界面体验是用 Swift 结合 AppKit 和 SwiftUI 编写的。标签页是原生标签页,分屏是原生 UI 组件,多窗口的表现也符合预期,等等。在 Linux 上,图形界面则使用 GTK,采用真正的 GTK 窗口和其他控件。
诸如错误提示这类功能,并非通过专门的终端视图实现,我们实际使用的是真正的原生 UI 组件。关键在于,虽然终端界面和核心逻辑是跨平台的,但用户交互都是为各个操作系统量身定制的,以实现真正的原生体验。

接下来快速概览一下技术栈和项目本身。
首先是项目。项目目前处于封闭测试阶段,并提供源码访问。之所以采用封闭测试,是因为这是一个个人项目,我不想让自己不堪重负。封闭测试让我可以慢慢邀请用户、修复他们遇到的问题,然后循环往复。
当项目最终发布时,它将是免费开源的,很可能会采用 GPL 许可证。我说“很可能”是因为欢迎测试者发表意见。目前一个正在积极讨论的话题是,是否选择更宽松的许可证,比如 MIT。
现在就有一个公开的 Discord 可以加入。我从该 Discord 中招募测试者。我也会不定期在个人博客上发布开发日志。
技术方面,我会在本次演讲中详细介绍其中的大部分内容,所以这里就不多说了。只需知道它是用 Zig 编写的,原生部分是真正的原生(例如 macOS 上的 AppKit),并且我们自己编写了很多依赖,比如事件循环。

好了,那么先从那个显而易见的问题开始:为什么选择 Zig?

简单来说:我喜欢这个社区、这门语言和它的构建系统。我认为这门语言和构建系统的特性非常适合这个终端项目,而本次演讲正是为了突出这一点。

你们想怎么猜都行,我不在乎这个问题,也不想回答它。我选择了 Zig,我喜欢 Zig,我们继续。

这里是按子系统划分的代码架构概览。这些是 Ghostty 中的主要子系统及其说明。下一张幻灯片我会展示可视化图表……

这里是将上述内容整合后的运行时架构。这张图中的所有内容都是 100% 用 Zig 编写的,除了 macOS 上的“apprt”,它部分使用了 Swift。
基本思路如下:
- 应用启动,入口在“apprt”中。
- Apprt 负责创建一个或多个“surface”(界面)。一个 surface 代表一个可交互的终端。Apprt 可以选择将其放入窗口、标签页、分屏等任意位置。这对核心来说并不重要!
- 每个 surface 会启动一个 IO 线程和一个渲染线程。
- IO 线程创建 pty 文件描述符并运行配置的命令(通常是 shell)。IO 线程负责读写 pty、处理终端事件如转义序列等。
- 渲染线程将终端状态转换为像素,并以固定的帧率绘制。渲染器还负责字体塑形和渲染。

尽管我对终端模拟器充满热情,但这里是 Zig Showtime,所以本次演讲的重点将转向我在 Ghostty 中使用的 Zig 模式。
这些模式不分先后。
给线上读者的说明:在这一部分,我大多是通过在实际的 Ghostty 源码中(自然是在 Ghostty 终端里)切换标签来展示这些模式的。如果将视频快进到这些幻灯片,就可以观看。

首先是 comptime 接口!

comptime 接口是指其实现会根据某些编译期已知信息而变化的值。
这里的源码是直接从 Ghostty 中提取的真实示例。此处展示的例子是依赖于某个构建选项的“字体”定义。可以看到,对于不同的构建时设置,我们会使用 CoreText,其他情况下使用 FreeType,我们甚至还支持基于网页 Canvas 的实现!
下方窗格展示了该接口的使用方式。使用这些接口的代码并不关心具体如何实现,只需面对一个已定义的接口并调用或使用它。

Comptime 接口是 Ghostty 实现平台相关功能的主要方式,用于字体、渲染器、应用运行时等。
其主要好处在于,对于那些永远不会在运行时改变的实现,你可以零运行时开销地替换实现。所有的字段访问和函数分发都在编译期确定。

Zig 编译器的一个特性是,它只会分析实际被引用的代码。这是一个特性,而非缺陷,因为它让特定场景的代码得以存在,而无需使用烦人的 #ifdef 式保护来将其隐藏。
缺点是,对于 comptime 接口,你必须确保测试所有构建选项,否则很容易引入构建失败。
Ghostty 在 CI 中会遍历所有构建选项。

这是我之前展示过的同一张幻灯片。我想再次展示它,以说明所有这些特性和模式所带来的能力。

这里是同样的子系统、目前已有的实现,以及未来可能实现的方案。几乎所有这些可替换的实现都是 comptime 接口。

接下来,还是 comptime,不过这次是数据表。

这是一个数据表的例子。在这个例子中,我们看到的是 Kitty 键盘协议的一些输入编码信息。
RawEntry 的元组形式在实际使用中并不友好,但对于轻松构建表格却非常方便。注意,这两者都不是 pub——它们本就不打算在文件外部使用。
有了这些非公开的条目,我们接下来对其进行处理……

利用 comptime,我们可以处理这些原始条目。在这个例子中,我们将原始条目的元组转换为更适合运行时使用的结构体。
转换过程完全在编译期发生,因此没有运行时开销。此外,由于原始条目从未在运行时代码中使用,它们不会在最终的构建产物中占用任何二进制空间。
在这个例子中,转换大多是直截了当的数据变换,但我们还能做更酷的事情……

这里是来自平台相关键码表的例子。我们的原始数据包含各个平台(Mac、Windows、通过 xkb 的 Linux,甚至 USB 码)的键码。但在运行时,我们会将其转换为只包含当前构建目标平台的 entries。
这样,我们的结构体更紧凑,最终的二进制文件更小,也无需在运行时进行这种条件查询。

我们还可以更进一步。这里是数据表中的一个例子,它定义了在给定模式下,对于给定的按键输入,终端应向正在运行的程序发送何种转义序列。
其中很多都遵循一种明显、重复的模式。与其手动逐一键入,我们可以编写一个在 comptime 执行的函数,以编程方式构建数据。
许多项目常常会用 shell、Python 或其他脚本来生成数据。而在 Zig 中,你可以在 comptime 中完成所有这些。
注意:在 pcStyle 中,函数体外的 comptime {} 包装是一个技巧,用于确保该函数永远不会在运行时被调用。

接下来,我们将在 comptime 数据表的基础上,聊聊 comptime 类型生成。

让我们先用 30 秒快速介绍一下 @Type 内建函数。很多人不知道它的存在。@Type 让你可以在 comptime 创建类型。它强大得离谱,但也吓人得离谱。

要小心!你应该将其使用限制在真正有意义的场景。要避免走向“完全元编程”,因为那会损害可读性并拖慢编译速度。

这个例子展示了 Ghostty 如何定义其支持的各种“模式”。模式是一种终端模式,正在运行的程序可以查询和设置它,从而改变终端模拟器的行为。共有数百种模式。
在这个例子中,我们采用了标准的模式:一个未导出的 entries 数据表。我们处理该表的方式之一,是使用 @Type 内建函数将所有键转换为一个完备的枚举。
我之所以没有预先定义该枚举,是因为每个条目都关联着额外的数据,我希望将它们紧凑地放在一起,便于编辑和查阅。在这个例子中,每个模式都有一个值。

这里展示了 FontIndex 结构的创建。该结构在 Ghostty 中用于引用特定字体。我们使用高位来表示样式(常规、粗体、斜体等),低位则表示数组的索引。
我们可以通过确定样式所需的位数,来精确计算索引所需的位数。
如果我们增加更多样式从而需要更多位,就会限制索引所能表示的字体数量。
注意:这会配合测试来确保我们拥有特定的可用大小,这样如果将来真的要增加 Style 的位宽,我们也会非常谨慎地进行。

接下来聊聊 Ghostty 如何与 Swift 集成。实际上,这是一个适用于任何能调用 C API 的语言的通用方案,而不仅仅是 Swift。

我们面临的问题是,现代 macOS 开发实际上离不开 Swift。是的,你可以用 Objective-C 构建出不错的应用,但苹果正在推出的所有新潮功能和框架都只支持 Swift,ObjC 的前景已经很明朗了。

在 Linux 上,Zig 直接使用 GTK 的 C API。Zig 调用 C API 的能力众所周知。问题在于 Swift(用于 Mac 应用)无法导出 C API,而 macOS 应用则期望拥有 main。
但是……Swift 可以调用 C。而 Zig 可以导出 C API。

所以我最终也将 Ghostty 打包成了一个嵌入式 C API。我将其编译为一个静态库,供基于 Swift 的 macOS 应用依赖,然后由 Swift 来调用该库。

例如,这里是 Swift 初始化 Ghostty 配置的代码。

通过这种方式,我们可以在不牺牲对 Linux 或其他未来平台可移植性的前提下,获得极其原生的 macOS 体验。
但更酷的是,Zig(尤其是 Zig 构建系统)让编写一个既能编译为可执行文件又能编译为库的程序变得如此简单。

未来,我计划将 C 库产物作为官方产物提供支持,让任何人都能在自己的应用中嵌入一个功能完整、特性丰富、快速、现代的终端。
但就目前而言,它尚未公开发布,仅用于 macOS 应用。

以上就是我们在 Ghostty 中使用的主要模式的概览。还有很多内容,但对于一次演讲来说已经足够了。其中一些模式本身或许就值得单独开一场演讲……
让我们再多聊一点 Ghostty,来为这次演讲收尾!

那么,项目的下一步是什么?
终端模拟能力已经相当完善。很少会遇到在 Ghostty 上无法运行、渲染错误等的程序,一旦发现,这类问题会被列为最高优先级。数十名测试者整天都在工作中使用 Ghostty,它是可靠的。
现在的重点主要在原生平台体验上,让应用越来越像为该平台量身定制。例如,在 macOS 上,我们正在开发图形化的设置窗口,而不仅仅是基于文件的设置。我们还在做诸如通过 iCloud 在 Apple 设备间同步配置(数据完全由你掌控)等功能。
还有更多……

Ghostty 已经很快了,但仍有巨大的改进空间。我们已知还有很多唾手可得的性能优化点,这令人兴奋,因为 Ghostty 本身就已经相当快了。
此外,我们希望对 VT 流解析器和处理器进行模糊测试。我们已经做过一些模糊测试并发现了几个 bug,所以肯定还有更多潜藏的问题。
还有各种我们从未测量过的性能领域,比如启动时间、输入延迟等。我们已尽量避免让这些代码路径变慢,但通过测量,我相信会发现一些大的优化机会。
Ghostty 目前的内存占用不算好。倒也不算糟糕,但称不上优秀。我花了很多时间和精力来确保 CPU 和渲染器性能出色,但在内存占用上有些松懈。这里有一些容易的优化点,例如我们目前会预先分配整个回滚缓冲区,占用几兆字节。改成动态分配会相当容易。

我们正在进行活跃的测试计划。要参与该计划,你必须加入 Discord。
我们的流程以稳定性为核心:我们会邀请一批测试者,在所有遗留问题解决之前,不会邀请下一批。当我说“问题”时,指的是 bug 或重大的缺失功能。我们不会实现每一个功能请求。

Ghostty 将在明年(2024 年)的某个时候公开发布。在那之前,我打算继续扩大测试计划,可能会扩展到数百人。
首个发布版本将是 1.0 版,最差也是 1.0 的候选发布版。如此高强度的测试计划的目标是,让我们一经推出就能拥有一个稳定、功能丰富的项目。
我之前提到过,Ghostty 将是自由开源软件,许可证很可能是 GPL。我说“很可能”是因为测试者仍有发言权,但目前的计划是 GPL。许多其他终端也是 GPL。对于 libghostty 与 GPL 的结合,确实存在一些顾虑,因此这仍是一个正在积极讨论的话题。
除了希望拥有稳定的软件外,发布推迟的另一个原因是我几周后就要迎来一个宝宝 我已经有了一个新生儿,我知道自己会非常非常忙碌,所以不想通过同时培育和启动一个完整的开源社区来给自己施加不必要的压力。
有些家长问我,有了新生儿在家,怎么还有时间写新的演讲稿、进行演讲并撰写这篇文章。实际上,这次演讲和这篇文章(除了像这样的一小部分备注外)都是在宝宝出生前就写好的。演讲本身在喂奶间隙进行并不算太难。是的,我很累。😊

❤️
随机一篇博客
评论
登录后参与讨论