演讲:介绍 Ghostty 及一些实用的 Zig 模式
这是我参加 Zig Showtime 一次演讲的文字版。如果你更想看视频,可以在 YouTube 上找到:Zig Showtime: Ghostty。视频末尾还包含一段问答环节,我没有收录在本文中。

大家好!今天我很兴奋能聊聊 Ghostty。Ghostty 是一个全新的终端模拟器,完全用 Zig 从零编写。
注意:在撰写本文时,Ghostty 仍未公开发布。我在演讲中多次提到这一点,但我想在开头就说明,以免有人一开始就产生误解。计划是 Ghostty 将免费开源,并于 2024 年的某个时候发布。我们目前正在运行一个封闭测试计划(提供源码访问权限)。我会在演讲的不同地方谈到这一点。

我叫 Mitchell!我就是那种热爱、热爱、无比热爱写代码的人。
我过去启动过一些流行的软件项目,你也许听说过,也许没有:Vagrant、Terraform、Vault 等等。不过我已经很多年没有参与过这些项目了。
我也是那种一直都有副项目在进行的人。一直如此。大约十年前,我写了一个炉石传说的日志分析器和回放软件,并把它卖给了一支职业电竞战队。几年前,我从零写了自己的邮件服务器,并用它为我的一些域名提供邮件服务。而现在,嗯,现在我有了这个终端模拟器。
我做这张幻灯片的时候正值晚饭时间,我真的很饿,所以决定干脆放一堆我吃东西的照片。我对食物之类的东西并没有特别的热情。我只是恰好在……此刻很饿。而且我喜欢这种能把整个房间串联起来的主题元素。

但这次演讲是关于 Ghostty 的!这个终端模拟器!
那么在继续之前,先看看 Ghostty 的样子!从这一张截图你就可以看出,Ghostty 的功能相当齐全!
Ghostty 是 macOS 和 Linux 上的原生应用。它有分屏、支持真彩色、运行 vim 毫无问题、有多种字体样式(粗体、斜体等)、支持 Kitty graphics protocol(Kitty 图形协议)等等。这还只是你在这张截图里能看到的功能!

如果你不知道什么是终端模拟器——你很可能用过——这里有几个例子。市面上还有几十个、几百个。

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

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

Ghostty 的目标是——在我看来已经实现了——同时具备所有特性。

快速、功能丰富、原生体验,这三者并非互斥。
我认为 Ghostty 如今的表现已经足以获得绿色对勾,但我们仍有很多可以做得更好的地方。
另外,指出一些警示信号:我对原生平台体验的标准是,应用的感觉和行为要像一个专门为该平台构建的原生应用。我不认为这个标准不合理。例如,我认为说 Alacritty 有点不够原生并不为过,因为新窗口会创建新进程。或者说 Kitty 有点不够原生,因为它的标签页使用了非原生控件。诸如此类(每一项都还有很多例子)。

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

但我想更进一步。我希望终端成为文本应用开发的现代平台,就像浏览器是 GUI 应用开发的现代平台一样(无论好坏)。
浏览器每年都会推出几十个(上百个?)新功能。它们快速创新,让应用开发者感到兴奋。这或许有点过头,但我认为终端可以比现在创新得更快。
这里列出了一些例子:
- 进度条!为什么我们每次都要(而且大多数时候画得很差)重绘进度条?
- 拖放功能,让编辑器和其他应用可以表现得更加原生。
- 标签页/分屏控制,让复用器能够利用现代终端模拟器的性能和功能。
- 鼠标手势,让 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 上,主要的 GUI 体验用 Swift 编写,使用 AppKit 和 SwiftUI。标签页是原生标签页,分屏是原生 UI 组件,多窗口的表现符合预期,等等。在 Linux 上,GUI 体验是 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 interface(编译期接口)!

comptime 接口是一个值,其实现会根据某些编译期已知的信息而变化。
这里的源代码是直接从 Ghostty 中提取的真实示例。这里展示的示例是一个“font face”的定义,它取决于某个构建选项。你可以看到,根据某个构建期设置,我们使用 CoreText,或者 FreeType,我们还支持基于 Web 的 Canvas 实现!
底部面板展示了接口的使用。使用这些接口的代码并不关心事情如何运作,存在某个定义好的接口,代码调用它或使用它即可。

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

Zig 编译器的一个特性是它只分析实际被引用的代码。这是特性而非缺陷,因为它让情境性代码可以存在,而不需要用讨厌的 #ifdef 式的守卫来把它隐藏起来。
缺点是对于 comptime 接口,你必须确保测试所有构建选项,否则很容易引入构建失败。
Ghostty 在 CI 中遍历所有构建选项。

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

这里是同样的那些子系统、如今存在的实现,以及未来可能的实现。几乎所有这些备选实现都是 comptime 接口。

接下来是更多 comptime 的用法,不过这次是数据表。

这是一个数据表的例子。在这个例子中,我们看到的是 Kitty Keyboard Protocol 的一些输入编码信息。
RawEntry 元组形式对实际使用来说不太友好,但对轻松构建表来说非常方便。注意这两者都不是 pub 的——它们并不打算在这个文件之外使用。
有了这些非 pub 的条目之后,我们对它们进行处理……

使用 comptime,我们可以处理这些原始条目。在这个例子中,我们把原始条目元组转换成对运行时更友好的结构体。
转换过程全部在编译期完成,所以没有运行时代价。此外,由于原始条目从不在运行时代码中使用,它们不会占用最终构建产物的任何二进制空间。
在这个例子中,转换主要是直接的数据变换,但我们还可以做更酷的事情……

这是平台特定键码表中的一个例子。我们的原始数据包含每个平台的键码(Mac、Windows、通过 xkb 的 Linux,甚至 USB 键码)。但对于运行时,我们把它转换成 entries,其中只包含我们正在构建的平台。
这样,我们的结构体更紧凑,最终的二进制文件更小,而且我们不需要在运行时做条件查找。

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

接下来我们将在 comptime 数据表的基础上,谈谈 comptime type generation(编译期类型生成)。

我们先来一个 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 配置的例子。

这样我们就能获得极其原生的 macOS 体验,同时不牺牲与 Linux 或其他未来平台的可移植性。
而格外酷的是,Zig(尤其是 Zig 构建系统)让编写一个既能编译成 exe 又能编译成库的程序变得多么容易。

未来,我计划把 C 库产物作为官方产物来支持,这能让任何人在他们的应用中嵌入一个功能完备、特性齐全、快速、现代的终端。
但就目前而言,它尚未发布,只用于 macOS 应用。

这就是 Ghostty 中我们使用的主要模式的巡礼。还有很多,但我觉得一场演讲讲这些就够了。其中一些模式或许本身就值得单独开一场演讲……
让我们在演讲的最后再多聊一点 Ghostty!

那么这个项目接下来要做什么?
终端模拟能力已经相当完善。极少数程序在 Ghostty 上无法运行或渲染不正确等情况,一旦发现就是最高优先级的 bug。几十名测试者整天用 Ghostty 做他们的专业工作,它是可靠的。
现在的重点主要放在原生平台体验上,让应用越来越像为该平台量身打造。例如,在 macOS 上,我们正在开发 GUI 设置窗口,而不仅仅是从文件读取设置。我们还在做 iCloud 配置在苹果设备间的同步(自己的数据自己掌控)之类的事情。
还有更多……

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

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

Ghostty 将于明年(2024 年)的某个时候公开发布。在那之前,我打算继续扩大测试计划,规模可能达到数百人。
第一个发布版本将是 1.0 版本,最差也是一个 1.0 候选版本。这个高强度测试计划的目标是,我们能够一发布就拿出一个稳定、功能丰富的项目。
我之前说过,Ghostty 将是 FOSS,许可证很可能是 GPL。我说“很可能”是因为测试者仍有发言权,但目前的计划是 GPL。许多其他终端也是 GPL。关于 libghostty 和 GPL 有一些具体的顾虑,所以这是一个活跃的讨论领域。
除了想要软件稳定之外,推迟的一部分原因是,我几周后就要迎来一个宝宝 我有了一个新生儿,我知道自己会非常非常忙,所以不想在尝试培育和启动一个完整的 OSS 社区的同时给自己施加过度的压力。
一些家长问过我,家里有新生儿的情况下,我怎么可能有时间写一场新演讲、做演讲,还写了这篇文章。演讲和这篇文章(除了像这样的一些备注)都是在我宝宝到来之前写好的。做演讲本身也不难,安排在两次喂奶之间就行。是的,我很累。😊

❤️
随机一篇博客