We Rewrote the Ghostty GTK Application

Mitchell Hashimoto

GhosttyのGTKアプリケーションを書き直しました

原文は Mitchell Hashimoto により に公開されました。 このブログを購読する

GhosttyのGTKアプリケーションを、ZigからGObjectタイプシステムを全面的に取り入れ、さらにValgrindで一歩一歩検証しながら全面的に書き直す作業が完了しました。その結果、LinuxとBSD上のGhosttyは、より高機能で安定し、保守しやすくなりました。

この過程からは興味深い技術的なトピックがいくつも得られましたが、ここでは特に2つに絞って取り上げたいと思います。(1) ZigからGObjectタイプシステムと連携する方法、(2) ValgrindでGTKアプリケーションを検証すること、そしてZigのコードベースでValgrindが見つけたメモリ問題についての考察です。

背景

まずは簡単に背景を説明します。Ghosttyはクロスプラットフォーム(macOS、Linux、FreeBSD)対応のターミナルエミュレータです。Ghosttyが他のクロスプラットフォームのターミナルエミュレータと一線を画しているのは、プラットフォームごとにネイティブなアプリケーション/GUIフレームワークを採用している点です1

macOS版のGhosttyはXcodeでビルドされた数千行規模のSwiftアプリケーションです。LinuxおよびBSD版のGhosttyは、X11Waylandなどとの直接的な統合を活用した数千行規模のGTKアプリケーションです。そしてこれらすべてを束ねているのが、Zigで書かれた非常に大規模な共有コアで、C ABI互換のAPIを公開しています。

Ghosttyがこれまでなぜそのような構成だったのか、そしてなぜ今回GTKアプリケーションを書き直すことにしたのか、その詳しい動機についてはオリジナルの「gtk-ng」PRをご覧ください。この記事では動機よりも、そこから得られた知見に焦点を当てます。

GObjectタイプシステムとZig

OOPやメモリ管理についてどう考えていようと、GTKを選ぶ以上、なんらかの形でGObjectタイプシステムと向き合わざるを得ないのが現実です。避けては通れません。

とはいえ、避けようと思えば避けられますし、実際に私たちは避けていました。その結果、参照カウントされないオブジェクトのライフタイムを、参照カウントされるオブジェクトのライフタイムに紐付けようとして混乱を招くことになりました。GhosttyのGTKアプリケーションでは、要するに「Zig側のメモリかGTK側のメモリのどちらか一方だけが解放され、もう一方が解放されていない」という種類のバグが繰り返し発生していました。

正確性の問題に加えて、オブジェクトシステムを避けたことで、シグナル(イベント)、プロパティ(GUI要素にバインドできるもの)、アクション(離れた場所から一方向の動作を呼び出す仕組み)といったGTKネイティブの機能も使いづらくなっていました。

具体例として、再読み込み可能な設定を見てみましょう。Ghosttyの設定は、Zigが所有するConfig構造体として表現されています。ウィンドウ、タブ、メニュー、スプリットなど、GUIのさまざまな部分がこの設定を認識する必要があります。

設定の再読み込みは、古いConfigを解放する前にGUI全体が更新されたことを保証しなければならなかったため、複雑で(比較的)CPU負荷が高く、エラーが起きやすい処理でした。

現在では、ZigのConfig構造体は参照カウントされるGhosttyConfig GObjectでラップされています。設定を再読み込みする際は、プロパティを上書きし、GObjectのプロパティ変更通知システムに、その変更をアプリケーション全体へ波及させること(時には複数のイベントループのティックにまたがって)を任せます。古い設定への参照がなくなれば、自動的に解放されます。概念的にははるかにシンプルです。

メモリ管理に加えて、カスタムGTKウィジェットもより簡単に作成できるようになりました。これにより、BlueprintのようなモダンなGTK UI技術を全面的に活用できるようになりました。例えば、こちらが私たちのターミナルウィンドウのBlueprintファイルです。このおかげで、新しいGTKタイトルバータブのオプションや、ベル通知時のアニメーション付きボーダーなど、GUI機能をより容易に導入できるようになりました。

GTKとZigにおけるValgrind

このトピックだけで一本のブログ記事が書けるほどですが、要点はこうです。最初のPRから最後のPRまで、すべての変更とGhosttyの機能をValgrindにかけ、メモリリークや未定義のメモリアクセスなどがないことを確認し、見つかった問題にはすべて対処してきました。

GTKアプリケーションでValgrindを実行するのはかなり厄介です。かなり大規模な抑制ファイルが必要になります。量が多いのは承知していますが、その80%はGTK自体が提供しているものです。残りは主にサードパーティのライブラリやGPUドライバによるものです。1つか2つ、疑わしいと感じてコメントを残した抑制もあります。

重要なのは、その過程で、間違いなく見逃されていたであろう数多くのバグを発見できたことです。例えば、dispose時にGObjectのWeakRefをクリアし忘れると、将来のある時点(数時間後、数日後になることもあります)に対象(参照先)のオブジェクトが破棄される際に、未定義のメモリアクセスが発生することを学びました。その未定義のメモリアクセスは99%の場合はたまたま問題なく動作しますが、時折クラッシュを引き起こします。楽しいですね! Valgrindはこれを問題なく検出してくれました。

メモリ安全性の話題は……ええと……特定の議論を活性化させるようです。なので、ここで2点述べておきます。

  1. 私たちのZigコードベースには、リークが1件と未定義のメモリアクセスが1件ありました。これは(良い意味で)本当に驚きでした。私たちのZigコードベースは大規模で複雑、かつパフォーマンスのために大量のメモリトリックを駆使しており、容易に危険な動作につながりかねません。正直、もっと多くの問題があると思っていました。しかも、見つかった1件のリークはサードパーティのC API呼び出し時に発生したもの(そのためZigでは検出できませんでした)でした。ですから、これは大きな成功です。

    Zigにはリーク検出機能付きのデバッグアロケータや、Ghosttyプロジェクトではデバッグビルドとテストビルドでのみ有効になるさまざまな安全性チェックがあります。さらに、ZigはValgrindとの統合も備えています。例えば、Zigで値をundefined(キーワード)に設定すると、ZigはValgrindのクライアントリクエストを発行してそのメモリを未定義としてマークします。これにより、さらに多くの問題を発見できます。

    今回の経験で、これらの保護がリリースビルドでは一切有効になっていないにもかかわらず、こうした仕組みが機能していることが本当によく分かりました。

  2. その他のすべてのメモリ問題は、C APIの境界に起因するものでした。私たちが見つけた他のすべての問題(数十件ありました)は、GObjectシステムの複雑なライフタイム、あるいはC APIの境界に直接関係するものでした。ここから得た教訓は、C APIをまたいで安全に呼び出すには(たとえそれがCで書かれていなくても)、Valgrindのようなツールが絶対に必要だということです。

    C APIを公開している複雑なライブラリの多くでは、C APIはオブジェクトのライフタイムが移譲されたり曖昧になったりする境界を表しています。それとやり取りするためにどの言語を使っていようと、保証される安全性は、APIのセマンティクスを理解し、適切なラッパーを書けるかどうかにかかっています。

Zigがメモリ安全性に関して提供する機能は十分に文書化されています。Zigが何をし、何をしないのか、そしてそれが良いか悪いかについては、学術的・理論的な議論が数多くあります。それらも価値ある議論ですが、経験的な結果も同様に価値があります。今回のプロセスは、大規模で複雑、マルチスレッド、マルチプラットフォームなZigプロジェクトにおいて、個々の機能すべてをValgrindで厳密に検証した経験的な結果を示しています。そこから何を読み取るかは皆さんにお任せします。論争を起こしたいわけではありません!

今後も、すべてのGTK関連PRをValgrindで実行し続ける予定です。また、メンテナやコントリビュータも同様に実行できるよう、プロジェクトのドキュメントを改善していきます(すでに数名が実行できるようになっています!)。

結論

これはGhosttyのGUI部分をゼロから書き直す5回目の経験になります。1回目はGLFWで、2回目はmacOSでSwiftUIを使って、3回目はmacOSでAppKitとSwiftUIを組み合わせて、4回目はLinuxでGTKを手続き的に使って、そして今回はLinuxでGTKと完全なGObjectタイプシステムを使っての書き直しです。

毎回、新しく価値あることを学び、その経験を次の反復へ(そしてプラットフォームを越えて)活かしてきました。今回もまた、macOS側に持ち帰りたい新しいテクニックをいくつか学びました。

また、GTKサブシステムのメンテナンスチーム全員がこの書き直しの完了に向けて参加し、多大な貢献をしてくれたことも強調しておきたいと思います。

新しく書き直されたGhostty GTKアプリケーションは、現在mainブランチからソースビルドする際のデフォルトとなっており、数週間後にリリースされるバージョン1.2ですべてのユーザーに提供される予定です。

脚注

  1. Linuxの人々は、私が「プラットフォームネイティブ」と言うとかなり熱くなります。Linuxにそのようなものは存在しませんが、GTKアプリ(あるいはQt)のようなものが、他のアプリケーションよりもほとんどのデスクトップで「ネイティブ」に感じられるという点では、分別のある人々は同意するでしょう。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント