我们重写了 Ghostty GTK 应用程序
摘要
Mitchell Hashimoto 详细介绍了 Ghostty 的 GTK 应用程序重写,以完全拥抱来自 Zig 的 GObject 类型系统,从而提高了稳定性、功能和内存安全性,并通过 Valgrind 验证。
暂无内容
查看缓存全文
缓存时间: 2026/05/16 03:39
# 我们重写了 Ghostty GTK 应用
来源:https://mitchellh.com/writing/ghostty-gtk-rewrite
我们刚刚完成了对 Ghostty GTK 应用的[重写](https://github.com/ghostty-org/ghostty/pull/8235),全面接纳了从 Zig 中使用的 [GObject 类型系统](https://docs.gtk.org/gobject/concepts.html),并在每一步都通过 [Valgrind](https://valgrind.org/) 进行了验证。结果是 Ghostty 在 Linux 和 BSD 上更加功能丰富、稳定且可维护。
这个过程涉及多个有趣的技术话题,但我想重点谈两点:(1) 从 Zig 与 GObject 类型系统的接口,以及 (2) 使用 Valgrind 验证 GTK 应用,并反思 Valgrind 在 Zig 代码库中发现的那些内存问题。
## 背景
首先,简单介绍一下背景。Ghostty 是一个跨平台(macOS、Linux、FreeBSD)的终端模拟器。Ghostty 与其他跨平台终端模拟器的不同之处在于,它为每个平台使用平台原生的应用或 GUI 框架¹。
在 macOS 上,Ghostty 是一个[数千行的 Swift 应用](https://github.com/ghostty-org/ghostty/tree/main/macos),使用 Xcode 构建。在 Linux 和 BSD 上,Ghostty 是一个[数千行的 GTK 应用](https://github.com/ghostty-org/ghostty/tree/b7913f09ad5326331192a24af53473ec541bf1af/src/apprt/gtk-ng),直接集成了 [X11](https://github.com/ghostty-org/ghostty/blob/main/src/apprt/gtk-ng/winproto/x11.zig)、[Wayland](https://github.com/ghostty-org/ghostty/blob/main/src/apprt/gtk-ng/winproto/wayland.zig) 等。将所有部分连接起来的是一个[用 Zig 编写的非常庞大的共享核心](https://github.com/ghostty-org/ghostty/tree/main/src),它导出了一个兼容 C ABI 的 API。
关于 Ghostty 之前为何如此设计以及我们为何决定现在重写 GTK 应用的完整动机,请参阅原始的 [“gtk-ng” PR](https://github.com/ghostty-org/ghostty/pull/7961)。本文将重点放在经验教训上,而非动机。
## GObject 类型系统与 Zig
无论你对 OOP 和内存管理有何看法,现实是如果你选择 GTK,你就必须*以某种方式*与 GObject 类型系统进行交互。你无法避免。
好吧,你*可以避免*,而我们*确实避免了*。但试图将非引用计数对象的生命周期与引用计数对象的生命周期绑定在一起,会导致一团糟。有一整类 bug 在 Ghostty GTK 应用中反复出现,可以概括为:Zig 内存或 GTK 内存已被释放,但并非两者同时释放。
除了正确性问题,回避对象系统还迫使我们无法使用 GTK 原生特性,如信号(事件)、属性(可由 GUI 元素绑定)、动作(从远处调用单向行为)等。
让我们看一个具体例子:可重新加载的配置。Ghostty 中的配置由一个 Zig 拥有的 `Config` 结构体表示。GUI 的许多不同部分都需要知道配置:窗口、标签页、菜单、分屏等。
重新加载配置是一项复杂、相对 CPU 密集且容易出错的任务,因为我们必须确保*整个 GUI* 更新后,才能释放旧的 `Config`。
现在,Zig 的 `Config` 结构体被包装在一个引用计数的 `GhosttyConfig` [GObject](https://github.com/ghostty-org/ghostty/blob/b7913f09ad5326331192a24af53473ec541bf1af/src/apprt/gtk-ng/class/config.zig) 中。当我们重新加载配置时,我们覆盖其属性,让 GObject 属性更改通知系统在整个应用(有时跨越多个事件循环周期)中传递。当旧配置不再有任何引用时,它就会被释放。概念上简单多了。
除了内存管理,我们现在还可以更轻松地创建自定义 GTK 控件。这让我们能够全面拥抱现代 GTK UI 技术,例如 [Blueprint](https://gitlab.gnome.org/GNOME/blueprint-compiler)。例如,这是我们的[终端窗口 Blueprint 文件](https://github.com/ghostty-org/ghostty/blob/b7913f09ad5326331192a24af53473ec541bf1af/src/apprt/gtk-ng/ui/1.5/window.blp)。这已经让我们更容易引入 GUI 特性,例如新的 GTK 标题栏标签页选项、响铃时动画边框等。
## Valgrind、GTK 与 Zig
这个话题本身值得一整篇博客文章。要点是,从第一个 PR 到最后一个,我们通过 Valgrind 运行了每一个更改和 Ghostty 特性,并处理了所有问题,以确保没有内存泄漏、未定义的内存访问等。
在 GTK 应用上运行 Valgrind 相当麻烦。我们需要一个[相当大的抑制文件](https://github.com/ghostty-org/ghostty/blob/main/valgrind.supp)。我知道这很多,但该文件的 80% 是由 GTK 本身提供的。其余的主要是第三方库和 GPU 驱动程序。可能有一两个抑制我觉得可疑(并在注释中注明了)。
重要的是,我们在这个过程中发现了一些*绝对*会被忽略的 bug。例如,我了解到如果你[在 dispose 过程中忘记清除 GObject `WeakRef`](https://github.com/ghostty-org/ghostty/commit/7548dcfe634cd9447e0b7a0f5e2900fe7094a225),它会在未来某个时刻(可能是数小时甚至数天!)导致*目标(被引用)*对象在 dispose 时发生未定义的内存访问。这种未定义的内存访问在 99% 的情况下*没问题*,但偶尔会导致崩溃。有趣!Valgrind 毫无问题地发现了这一点。
内存安全似乎会……呃……*激发*某些讨论。所以我想说两件事:
1. **我们的 Zig 代码库中只有一个泄漏和一个未定义内存访问。** 这*让我非常惊讶*(以好的方式)。我们的 Zig 代码库规模庞大、复杂,并且为了性能使用了大量内存技巧,这些技巧很容易导致不安全的行为。老实说,我原以为会有更多问题。此外,发现的唯一泄漏发生在调用第三方 C API 时(因此 Zig 无法检测到)。所以这是一个巨大的成功。Zig 有一个泄漏检测调试分配器和各种安全检查,这些检查仅在 Ghostty 项目的调试和测试版本中启用。此外,Zig 还与 Valgrind 集成。例如,当你将某个值在 Zig 中设置为 `undefined`(关键字)时,Zig 会发出一个 Valgrind 客户端请求,将该内存标记为未定义。这有助于发现更多问题。这次经历真正向我展示了这是*有效的*,尽管我们的发布版本中没有这些保护措施。
2. **所有其他内存问题都围绕着 C API 边界。** 我们发现的每一个其他问题(有几十个)都直接位于 GObject 系统的复杂生命周期中或 C API 边界上。我的结论是,你绝对需要像 Valgrind 这样的工具来安全地调用 C API(即使它们不是用 C 编写的)。对于大多数暴露 C API 的复杂库来说,C API 代表了一个边界,在这个边界上对象生命周期被转移或模糊。无论你使用何种语言与之交互,你所获得的安全性都仅取决于你对 API 语义的理解以及编写良好包装器的能力。
Zig 在内存安全方面提供的特性是有充分记录的。关于 Zig 做了什么或没做什么,以及这是好是坏,有许多学术或理论上的讨论。这些讨论是值得进行的,但实证结果也同样重要。这个过程展示了一个大型、复杂、多线程、多平台的 Zig 项目,在每一个单独特性都在 Valgrind 的严格检查下运行时,所得到的实证结果。你可以从中得出你自己的结论,我不想引发任何争论!
展望未来,我计划继续在 Valgrind 中运行每一个 GTK PR,并改进我们的项目文档,以便维护者和贡献者也能这样做(我们已经有一些人开始这样做了!)。
## 结论
这是我第五次从头编写 Ghostty 的 GUI 部分:一次用 GLFW,一次在 macOS 上用 SwiftUI,然后在 macOS 上用 AppKit 加 SwiftUI,一次在 Linux 上用 GTK 程序化方式,现在在 Linux 上用 GTK 和完整的 GObject 类型系统。
每一次,我都学到了新的、有价值的东西,并将这些经验带到了每一次迭代中(并跨平台应用)。即使是这一次,我也学到了一些新技巧,计划将其带回 macOS。
我还要强调,整个 GTK 子系统维护团队都积极参与,帮助完成了这次重写。他们也做了很多工作。
全新的、重写后的 Ghostty GTK 应用现在是当你从 `main` 分支源码构建 Ghostty 时的默认版本,并将在大约几周后的 1.2 版本中推送给所有用户。
---
1. 当我说“平台原生”时,Linux 用户会变得非常激动。Linux 上并没有这种东西,但理性的人一致认为,类似于 GTK 应用(或 Qt)在*大多数桌面环境*上比其它应用感觉更“原生”。 ↩
相似文章
Ghostty: 反思1.0版本的发布
Mitchell Hashimoto 反思了他用 Zig 构建的终端模拟器 Ghostty 达到1.0版本的过程,讨论了项目的起源、成功但富有争议的内测版,以及他对这款终端的愿景。
欢迎Ghostty子系统维护者
Mitchell Hashimoto宣布为开源终端模拟器Ghostty新增八位子系统维护者,并阐述了子系统治理模型及项目扩展目标。
Libghostty 即将到来
Mitchell Hashimoto 宣布了 libghostty 的计划,这是一个可嵌入的终端模拟库,首先推出 libghostty-vt,这是一个从 Ghostty 中提取的零依赖终端序列解析器。
Ghostty 即将离开 GitHub
Mitchell Hashimoto 宣布,由于持续的中断和挫败感,Ghostty 终端模拟器项目将离开 GitHub,此前他已有18年日常使用经历。
发现并修复Ghostty最大的内存泄漏
一篇详细的技术文章,介绍了诊断并修复Ghostty终端仿真器中一个重大内存泄漏的过程。该泄漏是由于在非标准页面大小下滚动缓冲区修剪逻辑错误导致的。修复已合并,将包含在即将发布的1.3版本中。