世界を動かす技術を、日本語で。

Ghostty GTKアプリケーションを再構築しました

概要

GhosttyのGTKアプリケーションをGObject型システムに完全対応させてZigから再実装。 Valgrindによる徹底的なメモリ検証で安定性と保守性を大幅向上。 ZigとGObject型システムの連携、Valgrindによるメモリ問題の発見が主な技術的焦点。 新構成でGUI機能追加やメモリ管理が格段に容易に。 今後もValgrind活用とドキュメント強化を継続予定。

Ghostty GTKアプリケーション再実装の背景

  • Ghosttyは macOS, Linux, FreeBSD対応 のクロスプラットフォームターミナルエミュレータ
  • 各プラットフォームで ネイティブGUIフレームワーク を採用
    • macOS:Swift + Xcode
    • Linux/BSD:GTK + X11/Wayland
    • コア部分は Zigで実装 し、C ABI互換APIとしてエクスポート
  • GTKアプリの再実装は 保守性・機能性・安定性向上 が目的

ZigとGObject型システムとの連携

  • GTK利用時は GObject型システムとの連携が不可避
  • 以前はGObjectを避けていたが、 寿命管理や機能制限 で問題が多発
    • 例:ZigメモリとGTKメモリの解放タイミング不一致によるバグ
  • GObject型システム採用により 参照カウントによるメモリ管理 が可能に
    • GhosttyConfig GObject でZigのConfig構造体をラップ
    • プロパティ変更通知 でGUI全体に設定変更を伝播
    • 参照がなくなった時点で自動解放、 メモリ管理が単純化
  • GTKネイティブ機能 (シグナル、プロパティ、アクション等)が活用可能に
  • Blueprint等の最新GTK UI技術 の採用でGUI機能追加が容易

ValgrindによるGTKアプリとZigコードのメモリ検証

  • 全てのPRと機能追加でValgrindによる検証を徹底
  • GTKアプリでのValgrind利用には 大規模なサプレッションファイル が必要
    • 80%はGTK公式提供、残りはサードパーティやGPUドライバ
  • Valgrindで 見逃されがちなバグを多数発見
    • 例:GObject WeakRefのクリア忘れによる未定義メモリアクセス
  • Zigコードベースのメモリ安全性
    • 発見された問題は 1件のリークと1件の未定義アクセス
    • リークはサードパーティC API由来、Zig自身の問題はほぼなし
    • Zigの デバッグアロケータやValgrind連携 機能が有効
  • 問題のほとんどは C API境界やGObjectの複雑なライフタイム管理 に起因
    • C APIは オブジェクト寿命の転送・曖昧化 の境界
    • 言語側の安全性はAPI理解とラッパー実装の質に依存

今後の展望とまとめ

  • 今後も 全てのGTK PRをValgrindで検証 し、 ドキュメント整備 を推進
  • GUI部分の再実装は 5回目 で、各回ごとに新たな知見を獲得
  • 今回の経験を macOS側にもフィードバック 予定
  • GTKサブシステム保守チームの協力 で再実装を完了
  • 新しいGhostty GTKアプリは mainブランチのデフォルト
    • 1.2リリース で全ユーザーに提供予定

Linuxにおける「プラットフォームネイティブ」について

  • Linuxでは「プラットフォームネイティブ」の定義が曖昧
  • 一般的には GTKやQtアプリ が「ネイティブ」感を持つとされる

Hackerたちの意見

プログラミングが良いシステムに合わせることが大事だっていういい例だね。OOPやメモリ管理についてどう思うかは別として、GTKを選ぶとGObject型システムと何らかの形でインターフェースしなきゃならないのが現実なんだ。避けられないよ。まあ、避けることもできるけど、実際に避けたら、参照カウントされてないオブジェクトのライフタイムを参照カウントされてるオブジェクトに結びつけるのがめちゃくちゃになるんだ。Ghostty GTKアプリケーションでは、基本的に「ZigメモリかGTKメモリが解放されたけど、両方ではない」というバグが頻繁に発生してたよ。

OOPやメモリ管理についてどう思うかは別として、GTKを選ぶとGObject型システムと何らかの形でインターフェースしなきゃならないのが現実なんだ。避けられないよ。過去に、これがGTKに対する私の評価でもあった。だから、GTKを直接使わない道を選ぶことにしたんだ。アプリケーション間で統一されたユーザーインターフェースがあることの価値は理解してるけど、GTKが提供する機能はその代償に見合わないと思ってる。GTKベースのオープンソースアプリで周辺をいじってきたけど、GTKとGObjectシステムは自分の意見とはあまり相容れない意見を持ってるように感じる。GTKが存在することを嫌ってるわけじゃないし、使わない選択をしてるのも全然問題ない。でも、使わない選択をしてることを理解してない人もいるみたい。世の中には他にもたくさんのGUIツールキットがあって、その中でもGTKは一番洗練されてる方だと思う。GTKがもっと支配的でなければ、GTKを磨くために使われるリソースが、もっと良い基盤アーキテクチャを持つ別のフレームワークを磨くために使われてたんじゃないかって気がする。もちろん、私が考える「良い」は他の人が考える「良い」とは限らないけど。GTKを使ってる人の中で、どれだけの人が嫌々使ってて、どれだけの人が「これがベストなツールだ!」って思ってるんだろう?

GTKとGObjectシステムは自分の意見とはあまり相容れない意見を持ってるように感じる。これを言うのは面白いかもしれないけど…私もそう感じてる。Gnomeエコシステムの視点にはかなり反対だよ。面白いね!LinuxでGTKを使うのは実用的な選択だった。Ghosttyの目標は「プラットフォームネイティブ」であること(Linuxにはそんなものはないから、ここで定義してる)なんだ。GTKは様々な定義でLinuxで最も人気があり、広く使われているGUIツールキットで、アプリがほとんどのエコシステムにフィットするようにしてくれる。だからGTKを選んだんだ。libghosttyが他のアプリ(サードパーティがメンテナンスするもの、私にとってmacOSとGTKを維持するのは大変だから)を生み出すことを願ってる。そうすれば、強制されることはないからね。例えば、WraithはWaylandネイティブのGhosttyフロントエンド(GTKなし)だよ: https://github.com/gabydd/wraith すごいね。

僕の考えでは、GTKがLinuxで注目されている主な理由は一つだけ:Cバインディングがあるから。だから、ほぼ全ての言語に対して decent なバインディングがあるんだよ。特別なことをしようとしなければ、これらのバインディングは自動生成もできる。対抗馬のQtはC++やPythonにかなり依存していて、逆に不利になってる。開発者がいる場所に合わせて、特定の言語を使うように強制するんじゃなくて、彼らに寄り添うべきだよ。それに、結局のところ、フル機能の複雑なデスクトップアプリを作るなら、古典的な命令型UIツールキットが実用的な選択肢の一つだと思う。十分にテストされた、アクセスしやすいウィジェットが揃っていて、その落とし穴もよく知られてるからね。新しいアプローチは、すべてを自分で書いたりインポートしたりすることを期待していて、ある程度の複雑さを超えると開発者に余計な負担をかけるんだ。

ここ数ヶ月、Ghosttyを使っててめっちゃ満足してる。今ではmacOSのターミナルとしてはこれがメインだよ。Mitchellとチームの皆さん、頑張ってくれてありがとう!

RustがこのシナリオでZigの代わりになったら、メモリの正しさのエラーを防げたのか気になるな。大半がZig/Cの相互作用によるものだったみたいだから、Rustでも同じ問題が起きたんじゃないかと思うけど、Go開発者としてはただの推測だよ。Cと大量にやり取りしてる時に、正しさを確保するためのツールがもっとある言語があるのか興味あるな。

記事の主なポイントの一つは、彼らが遭遇したメモリの問題が驚くほど少ないことについてだったと思う。彼はZigとValgrindの相性がすごく良いって話してる。

こんにちは @schmichael ;) Rustなら一つは防げたね。残りはRustでは防げなかったと思う。もう気づいてるように、それはC APIの境界層とセマンティクスにあったから。Rustラッパーの安全性に依存してたんだ。一つの議論としては、より豊かで実績のあるラッパーライブラリのエコシステムが、私のDIYラッパーよりもそれを防げたかもしれないってこと。Rustが防げたのは、単純な未定義メモリアクセスだった: https://github.com/ghostty-org/ghostty/pull/7982 (少なくとも、Rustならこれをキャッチできたと思う)。実際には、最初のレンダリングフレームでゴミメモリをコピーしてたけど、そのメモリは使われてなかったし、どこにも送られてなかったから、実際にはほとんど安全だった。でも、正しくはないよね!

GTKでは作業したことがないけど、ここで説明してることは、Zigで良いAPIを持つGodotバインディングを作ろうとしてる時に直面してることに似てる気がする。プロジェクトは進行中だけど、Godotは - OOPの概念がたくさんある:クラス、仮想メソッド、プロパティ、シグナルなど - それらの概念を扱うためのC APIがある - エンジンオブジェクトのライフタイムを管理する(どれにもユーザーデータを付けられる) - 参照カウントされたオブジェクトの木がある これをZigのイディオムに最適なAPIで結びつけるのは大変な頭痛の種なんだ(特にライフタイムの扱い)。かなり進んできたけど、何か追加の洞察やコードスニペットがあれば教えてほしい。これに取り組んで作ったライブラリがこれなんだけど、あまり誇りには思ってない: https://github.com/gdzig/oopz 現在のAPIの状態を示すスニペットもあるよ: https://github.com/gdzig/gdzig/blob/master/example/src/Signa... あと、GhosttyのフロントエンドをGodotの拡張として書いてみたいな。

改善されてるといいけど、最後に言語用のGTKバインディングを書いたときは本当に miserable だった。98%はまともだったけど、残りの2%は「この関数がこのオブジェクトの参照を取るかどうかは、渡された他のパラメータによる」みたいなことがあって、ライフネス分析が「面白い」ことになったよ。

これが進行中のプロジェクトだとは知らなかったけど、すごくワクワクするね。Godotが大好きだし、Zigも結構好きなんだ。これからも注目していくよ。

仕事で似たような経験をしたことがあるよ。F#を使ってFirestoreと連携するGoogle Cloudサービスを作ったんだけど、FirestoreのライブラリはC#用に作られてるから、結構苦労した。まあ、そこそこ動いたけど、ラッピング関数をたくさん作らないと、F#らしい書き方ができなかったんだよね。

GTKのタイプシステムに全く干渉されずに、かなり大きなGTKアプリを作ることができたよ。要は、彼らが継承してほしいところにラムダをたくさんフックするだけで、自分のクラスを拡張して、全ての部分が連携できるようにしたんだ。最終的にはそんなにごちゃごちゃしなかったけど、教条的なGTKアプリを書いてる人には混乱するかもね。

これにより、新しいGTKタイトルバーのタブオプションなどのGUI機能を簡単に導入できるようになりました。 そうそう!これは大きいよ。以前、タイトルバーがノートパソコンの画面でスペースを無駄にするからGhosttyを諦めたんだ。: https://bsky.app/profile/reillywood.bsky.social/post/3lebapf... 新機能がどんな感じか気になる人のためにPRも見つけたよ: https://github.com/ghostty-org/ghostty/pull/8166

最大化されていなければ、スペースは無駄にならないよ。レスポンス画像のタイトルはすごく混雑してるね。タイトルバーからいくつかボタンを削除できれば、まあ大丈夫かも。

GhosttyやAlacritty、WezTerm、ZedみたいなGPUベースのアプリを使ってるんだけど、もちろん速くていいからね… 皮肉なことに、これらのおかげでDXが悪化したよ。NVIDIAのドライバーが、古いRegolithのi3wmや新しいsway/waylandのセットアップでどれだけひどく動いてるかを浮き彫りにしちゃった。マジでひどい。Claudeが思いつく限りの魔法の環境フラグを試したし、550/560/575/580のドライバーのバージョンも4つ試したけど、どれも画面共有やスリープからの復帰、バグやクラッシュ、セグメンテーションフォルトがひどかった。ずっとこんなに悪かったんだろうけど、GPUを実際に使ってなかったから気づかなかったのかな?笑

Waylandでも似たような経験をしたよ。X11で合成効果をオフにすると、1050Tiのマシンと、古くて「radeon」ドライバーが必要なAMDカードのマシンの両方で問題なく動いた。Waylandは遅延したり、クラッシュしたり、ゴチャゴチャした出力を表示したりしてた。

"また、すべてのステップでValgrindで確認している" これは…めっちゃ明白だけど、今までやったことも考えたことも、他の誰かがやってるのも見たことがない。自分が出会ったり始めたりしたValgrindの使用は、特定のバグやパフォーマンスの退化によって引き起こされたものばかりだった。開発プロセスの一環としてValgrindスイート(少なくともMemcheckとHelgrind)を積極的に使うことで、ほとんどのツールの安定性が大幅に向上するだろうし、バグを見つけるのもずっと簡単になると思う(バグが導入された時点で見つけられるから、何百回もコミットした後じゃなくてね)。