Talk: Introducing Ghostty and Some Useful Zig Patterns

Mitchell Hashimoto

演讲:介绍 Ghostty 及一些实用的 Zig 模式

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

幻灯片 1

大家好!今天我很兴奋能聊聊 Ghostty。Ghostty 是一个全新的终端模拟器,完全用 Zig 从零编写。

注意:在撰写本文时,Ghostty 仍未公开发布。我在演讲中多次提到这一点,但我想在开头就说明,以免有人一开始就产生误解。计划是 Ghostty 将免费开源,并于 2024 年的某个时候发布。我们目前正在运行一个封闭测试计划(提供源码访问权限)。我会在演讲的不同地方谈到这一点。


幻灯片 2

我叫 Mitchell!我就是那种热爱、热爱、无比热爱写代码的人。

我过去启动过一些流行的软件项目,你也许听说过,也许没有:Vagrant、Terraform、Vault 等等。不过我已经很多年没有参与过这些项目了。

我也是那种一直都有副项目在进行的人。一直如此。大约十年前,我写了一个炉石传说的日志分析器和回放软件,并把它卖给了一支职业电竞战队。几年前,我从零写了自己的邮件服务器,并用它为我的一些域名提供邮件服务。而现在,嗯,现在我有了这个终端模拟器。

我做这张幻灯片的时候正值晚饭时间,我真的很饿,所以决定干脆放一堆我吃东西的照片。我对食物之类的东西并没有特别的热情。我只是恰好在……此刻很饿。而且我喜欢这种能把整个房间串联起来的主题元素。


幻灯片 3

但这次演讲是关于 Ghostty 的!这个终端模拟器!

那么在继续之前,先看看 Ghostty 的样子!从这一张截图你就可以看出,Ghostty 的功能相当齐全!

Ghostty 是 macOS 和 Linux 上的原生应用。它有分屏、支持真彩色、运行 vim 毫无问题、有多种字体样式(粗体、斜体等)、支持 Kitty graphics protocol(Kitty 图形协议)等等。这还只是你在这张截图里能看到的功能!


幻灯片 4

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


幻灯片 5

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


幻灯片 6

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


幻灯片 7

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


幻灯片 8

快速、功能丰富、原生体验,这三者并非互斥

我认为 Ghostty 如今的表现已经足以获得绿色对勾,但我们仍有很多可以做得更好的地方。

另外,指出一些警示信号:我对原生平台体验的标准是,应用的感觉和行为要像一个专门为该平台构建的原生应用。我不认为这个标准不合理。例如,我认为说 Alacritty 有点不够原生并不为过,因为新窗口会创建新进程。或者说 Kitty 有点不够原生,因为它的标签页使用了非原生控件。诸如此类(每一项都还有很多例子)。


幻灯片 9

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


幻灯片 10

但我想更进一步。我希望终端成为文本应用开发的现代平台,就像浏览器是 GUI 应用开发的现代平台一样(无论好坏)。

浏览器每年都会推出几十个(上百个?)新功能。它们快速创新,让应用开发者感到兴奋。这或许有点过头,但我认为终端可以比现在创新得更快。

这里列出了一些例子:

  • 进度条!为什么我们每次都要(而且大多数时候画得很差)重绘进度条?
  • 拖放功能,让编辑器和其他应用可以表现得更加原生。
  • 标签页/分屏控制,让复用器能够利用现代终端模拟器的性能和功能。
  • 鼠标手势,让 TUI 可以响应轻扫、多点触控、惯性等等。
  • 安全特性:终端转义序列如今是个危险地带。这个话题以后再谈。我们可以做得更好。
  • 还有更多!

幻灯片 11

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。


幻灯片 12

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


幻灯片 15

在 macOS 上,主要的 GUI 体验用 Swift 编写,使用 AppKit 和 SwiftUI。标签页是原生标签页,分屏是原生 UI 组件,多窗口的表现符合预期,等等。在 Linux 上,GUI 体验是 GTK,使用真正的 GTK 窗口和其他控件。

像错误消息这样的功能并不是用专门的终端视图实现的,我们实际上使用真正的原生 UI 组件。重点是,虽然终端界面和核心逻辑是跨平台的,但用户交互都是为每个操作系统专门构建的,以实现真正的原生体验。


幻灯片 16

我们来快速概览一下技术栈和项目本身。

首先是项目。该项目目前处于提供源码访问权限的封闭测试阶段。我们之所以做封闭测试,是因为这是一个个人项目,我不想把自己压垮。封闭测试让我可以慢慢邀请一些人、修复他们的问题,然后循环往复。

项目最终发布时,将是免费且开源的,很可能采用 GPL 许可证。我说“很可能”是因为欢迎测试者表达意见。目前一个活跃的讨论话题是,或许会选择更宽松的许可证,例如 MIT。

现在就有一个你可以加入的公开 Discord。我从那个 Discord 中招募测试者。我还会不定期在我的个人博客上写开发日志。

技术方面,我会在这次演讲中详细介绍其中大部分内容,所以这里不多花时间。你只需要知道:它是用 Zig 写的,原生部分是真正的原生(即 macOS 上的 AppKit),而且我们自己编写了很多依赖项,比如事件循环。


幻灯片 17

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


幻灯片 18

简单来说:我喜欢这个社区、这门语言和它的构建系统。我认为语言和构建系统的特性非常适合这个终端项目,这次演讲就是要专门展示这一点。


幻灯片 19

随便你怎么猜测,我不关心这个问题,也不打算回答。我选择了 Zig,我喜欢 Zig,我们继续。


幻灯片 20

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


幻灯片 21

这是把这些整合起来的运行时架构。这张图里的一切都是 100% Zig,除了 macOS 上的 "apprt",它部分是 Swift。

基本思路是:

  1. 应用启动,入口点在 "apprt"。
  2. Apprt 负责创建一个或多个“surface”。surface 是任何代表单个可交互终端的东西。Apprt 可以选择把它放进窗口、标签页、分屏等等。这对核心来说无所谓!
  3. 每个 surface 启动一个 IO 线程和一个渲染线程。
  4. IO 线程创建 pty 文件描述符并运行配置的命令(通常是 shell)。IO 线程负责读写 pty、处理转义序列等终端事件等等。
  5. 渲染线程以某个固定帧率转换终端状态并绘制像素。渲染器还负责字体的整形和渲染。

幻灯片 22

尽管我对终端模拟器充满热情,但这里是 Zig Showtime,所以这次演讲的重点将转向我在 Ghostty 中使用的 Zig 模式。

这些模式的排列没有特定顺序。

给在线读者的说明:这一节的大部分内容我切换到了终端窗口,在实际的 Ghostty 源码中展示这些模式(当然,是在 Ghostty 终端里)。如果你把视频快进到这些幻灯片,就可以观看。


幻灯片 23

第一个,comptime interface(编译期接口)!


幻灯片 24

comptime 接口是一个值,其实现会根据某些编译期已知的信息而变化。

这里的源代码是直接从 Ghostty 中提取的真实示例。这里展示的示例是一个“font face”的定义,它取决于某个构建选项。你可以看到,根据某个构建期设置,我们使用 CoreText,或者 FreeType,我们还支持基于 Web 的 Canvas 实现!

底部面板展示了接口的使用。使用这些接口的代码并不关心事情如何运作,存在某个定义好的接口,代码调用它或使用它即可。


幻灯片 25

comptime 接口是 Ghostty 实现平台特定功能的主要方式,用于字体、渲染器、应用运行时等等。

主要的好处是,对于在运行时永远不会改变的实现,你可以在零运行时开销的情况下替换实现。所有字段访问和函数分发都在编译期确定。


幻灯片 26

Zig 编译器的一个特性是它只分析实际被引用的代码。这是特性而非缺陷,因为它让情境性代码可以存在,而不需要用讨厌的 #ifdef 式的守卫来把它隐藏起来。

缺点是对于 comptime 接口,你必须确保测试所有构建选项,否则很容易引入构建失败。

Ghostty 在 CI 中遍历所有构建选项。


幻灯片 27

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


幻灯片 28

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


幻灯片 29

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


幻灯片 30

这是一个数据表的例子。在这个例子中,我们看到的是 Kitty Keyboard Protocol 的一些输入编码信息。

RawEntry 元组形式对实际使用来说不太友好,但对轻松构建表来说非常方便。注意这两者都不是 pub 的——它们并不打算在这个文件之外使用。

有了这些非 pub 的条目之后,我们对它们进行处理……


幻灯片 31

使用 comptime,我们可以处理这些原始条目。在这个例子中,我们把原始条目元组转换成对运行时更友好的结构体。

转换过程全部在编译期完成,所以没有运行时代价。此外,由于原始条目从不在运行时代码中使用,它们不会占用最终构建产物的任何二进制空间。

在这个例子中,转换主要是直接的数据变换,但我们还可以做更酷的事情……


幻灯片 32

这是平台特定键码表中的一个例子。我们的原始数据包含每个平台的键码(Mac、Windows、通过 xkb 的 Linux,甚至 USB 键码)。但对于运行时,我们把它转换成 entries,其中只包含我们正在构建的平台。

这样,我们的结构体更紧凑,最终的二进制文件更小,而且我们不需要在运行时做条件查找。


幻灯片 33

我们还可以更进一步。这是数据表中的另一个例子,它定义了在给定模式下,对于给定的按键输入,终端应该向正在运行的程序发送什么转义序列。

其中很多都遵循明显且重复的模式。与其手动把它们全部敲出来,我们可以编写一个在 comptime 执行的函数,以编程方式构建这些数据。

很多项目常常用 shell、Python 或其他脚本来做数据生成。而在 Zig 中,你可以在 comptime 里全部搞定。

注意:pcStyle 中函数体外面包裹的 comptime {} 是一个技巧,用于确保这个函数永远不可能在运行时被调用。


幻灯片 34

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


幻灯片 35

我们先来一个 30 秒的 @Type 内置函数教程。很多人不知道它的存在。@Type 内置函数让你能在 comptime 创建类型。它强大得离谱,但也可怕得离谱


幻灯片 36

小心使用!你应该把它的使用限制在合理的情况下。你需要小心,不要走向“完全元编程”,因为那会损害可读性并拖慢编译速度。


幻灯片 37

这个例子展示了 Ghostty 如何定义它支持的各种“模式”。模式是一种终端模式,正在运行的程序可以查询和设置它,以改变终端模拟器的行为。模式有几百个。

在这个例子中,我们有标准模式的做法:一个未导出的 entries 数据表。我们处理这个表的方式之一,是使用 @Type 内置函数把所有键转换成一个穷举枚举。

我不预先定义枚举的原因是,每个条目都有额外的关联数据,我想把它们紧凑地放在一起,便于编辑和查阅。在这个例子中,每个模式都有一个值。


幻灯片 38

这展示了 FontIndex 结构体的创建。这个结构体在 Ghostty 中用来引用某个特定字体。我们用高位来表示样式(常规、粗体、斜体等),用低位来表示指向数组的索引。

我们可以通过确定样式所需的位数,来得出索引所需的确切位数。

如果我们添加更多样式从而增加更多位,就会限制索引能表示的字体数量。

注意:这配有测试,以确保我们有特定的可用大小,这样如果将来真的增加 Style 的位宽,我们会是非常有意识地去做这件事。


幻灯片 39

接下来我们谈谈 Ghostty 如何与 Swift 集成。实际上,这是一个对任何能调用 C API 的语言都适用的更通用方案,不仅仅是 Swift。


幻灯片 40

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


幻灯片 41

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

但是……Swift 可以调用 C。而 Zig 可以导出 C API。


幻灯片 42

于是我又把 Ghostty 封装成了一个嵌入式 C API。我把它编译成一个静态库,我的基于 Swift 的 macOS 应用依赖它,然后 Swift 调用这个库。


幻灯片 43

这里就是 Swift 初始化 Ghostty 配置的例子。


幻灯片 44

这样我们就能获得极其原生的 macOS 体验,同时不牺牲与 Linux 或其他未来平台的可移植性。

格外酷的是,Zig(尤其是 Zig 构建系统)让编写一个既能编译成 exe 又能编译成库的程序变得多么容易。


幻灯片 45

未来,我计划把 C 库产物作为官方产物来支持,这能让任何人在他们的应用中嵌入一个功能完备、特性齐全、快速、现代的终端。

但就目前而言,它尚未发布,只用于 macOS 应用。


幻灯片 46

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

让我们在演讲的最后再多聊一点 Ghostty!


幻灯片 47

那么这个项目接下来要做什么?

终端模拟能力已经相当完善。极少数程序在 Ghostty 上无法运行或渲染不正确等情况,一旦发现就是最高优先级的 bug。几十名测试者整天用 Ghostty 做他们的专业工作,它是可靠的。

现在的重点主要放在原生平台体验上,让应用越来越像为该平台量身打造。例如,在 macOS 上,我们正在开发 GUI 设置窗口,而不仅仅是从文件读取设置。我们还在做 iCloud 配置在苹果设备间的同步(自己的数据自己掌控)之类的事情。

还有更多……


幻灯片 48

Ghostty 很快,但仍有巨大的提升空间。仍有很多我们已知的唾手可得的性能优化,这令人兴奋,因为 Ghostty 已经相当快了。

此外,我们想对我们的 VT 流解析器和处理器进行模糊测试。我们做过一些模糊测试并发现了几个 bug,所以我确信还有更多潜伏的问题。

还有一些性能领域我们从未测量过,比如启动时间、输入延迟等等。我们一直尽量避免劣化这些代码路径,但通过测量,我相信我们会发现一些巨大的优化空间。

Ghostty 目前的内存占用不算理想。它并不糟糕,但也不算好。我花了大量时间和精力确保 CPU 和渲染器性能出色,但在内存使用上有点松懈。这里有一些容易的优化,例如我们目前会预分配整个回滚缓冲区,有好几兆字节。把它改成动态分配应该相当容易。


幻灯片 49

我们正在运行一个活跃的测试计划。要加入该计划,你必须加入 Discord。

我们的流程以稳定性为核心:我们邀请一定数量的测试者,在所有未决问题解决之前不会邀请下一批。我说的“问题”是指 bug 或重大功能缺失。我们不会实现每一个功能请求。


幻灯片 50

Ghostty 将于明年(2024 年)的某个时候公开发布。在那之前,我打算继续扩大测试计划,规模可能达到数百人。

第一个发布版本将是 1.0 版本,最差也是一个 1.0 候选版本。这个高强度测试计划的目标是,我们能够一发布就拿出一个稳定、功能丰富的项目。

我之前说过,Ghostty 将是 FOSS,许可证很可能是 GPL。我说“很可能”是因为测试者仍有发言权,但目前的计划是 GPL。许多其他终端也是 GPL。关于 libghostty 和 GPL 有一些具体的顾虑,所以这是一个活跃的讨论领域。

除了想要软件稳定之外,推迟的一部分原因是,我几周后就要迎来一个宝宝 我有了一个新生儿,我知道自己会非常非常忙,所以不想在尝试培育和启动一个完整的 OSS 社区的同时给自己施加过度的压力。

一些家长问过我,家里有新生儿的情况下,我怎么可能有时间写一场新演讲、做演讲,还写了这篇文章。演讲和这篇文章(除了像这样的一些备注)都是在我宝宝到来之前写好的。做演讲本身也不难,安排在两次喂奶之间就行。是的,我很累。😊


幻灯片 51

❤️

原文由 Mitchell Hashimoto 发布

本文章由 stealth/ox-alpha 进行翻译