GhosttyのGTKアプリケーションを書き直しました
先ほど完了したところですが、GhosttyのGTKアプリケーションを書き直し、ZigからGObjectの型システムを全面的に活用するとともに、すべての段階でValgrindによる検証も行いました。その結果、LinuxとBSD上のGhosttyは、より機能が充実し、安定性と保守性も高まりました。
この作業には興味深く技術的な話題がいくつもありますが、ここでは2つに絞ります。(1)ZigからGObjectの型システムと連携する方法、(2)GTKアプリケーションをValgrindで検証する方法と、ValgrindがZigのコードベースで見つけたメモリ関連の問題について考えたことです。
背景
まずは簡単に背景を説明します。Ghosttyはクロスプラットフォーム(macOS、Linux、FreeBSD)のターミナルエミュレーターです。Ghosttyがほかのクロスプラットフォームのターミナルエミュレーターと一線を画しているのは、各プラットフォームでネイティブのアプリケーションまたはGUIフレームワークを使っている点です1。
macOSでは、GhosttyはXcodeで構築された数千行規模のSwiftアプリケーションです。LinuxとBSDでは、Ghosttyは数千行規模のGTKアプリケーションで、X11やWaylandなどと直接連携しています。そして全体をつないでいるのが、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つだけ言わせてください。
私たちのZigコードベースにあったのは、リーク1件と未定義のメモリアクセス1件でした。これは本当に驚きでした(良い意味で)。私たちのZigコードベースは大規模で複雑ですし、パフォーマンスのために、危険な動作につながりかねないメモリ上の技巧も大量に使っています。正直、もっとたくさん問題が見つかると思っていました。しかも、見つかったリークはサードパーティーのC APIを呼び出した際に発生したものだったので、Zigでは検出できませんでした。これは大成功です。
GhosttyプロジェクトのZigには、リークを検出するデバッグアロケーターと、デバッグビルドおよびテストビルドでのみ有効になるさまざまな安全性チェックがあります。さらに、ZigはValgrindとも統合されています。たとえば、Zigで
undefined(キーワード)として値を設定すると、Zigは一部のメモリを未定義としてマークするValgrindのクライアントリクエストを出力します。これにより、さらに多くの問題を見つけられます。今回の経験から、リリースビルドにはこうした保護機能を一切含めていないにもかかわらず、これらが機能していることがよく分かりました。
それ以外のメモリ問題は、すべてC APIとの境界に関するものでした。ほかに見つかった問題(数十件ありました)は、すべてGObjectシステムの複雑なライフタイム管理、またはC APIとの境界に直接関係していました。ここから得た教訓は、C APIをまたいで安全に呼び出すには(たとえそのAPIがCで書かれていなくても)、Valgrindのようなツールが絶対に必要だということです。
C APIを公開している複雑なライブラリの多くでは、C APIがオブジェクトのライフタイムが移譲されたり、曖昧になったりする境界になります。どの言語で連携するにせよ、保証される安全性は、APIの意味論を理解し、適切なラッパーを書くことによってのみ確保できます。
Zigがメモリ安全性のために提供している機能については、よく文書化されています。Zigが何をするのか、何をしないのか、それが良いのか悪いのかについては、学術的または理論的な議論が数多くあります。そうした議論には価値がありますが、実証的な結果にも同じ価値があります。今回の過程では、大規模で複雑、マルチスレッド、マルチプラットフォームのZigプロジェクトで、個々の機能をすべてValgrindの下で厳密に実行した場合の実証結果が得られました。そこから何を読み取るかはお任せします。炎上合戦を始めたいわけではありません!
今後も、すべてのGTK PRをValgrind上で実行し続けるつもりです。また、メンテナーやコントリビューターも同じことができるように、プロジェクトのドキュメントを改善していきます(すでにいくつかは動いています!)。
結論
GhosttyのGUI部分をゼロから書くのは、今回で5回目です。最初はGLFW、次にmacOSでSwiftUI、その次はmacOSでAppKitとSwiftUI、その次はLinuxで手続き的なGTK、そして今回はLinuxでGTKとGObjectの型システム全体を使いました。
そのたびに新しく価値のあることを学び、その経験を次の実装(そして別のプラットフォーム)に持ち込んできました。今回でさえ、macOSにも持ち帰るつもりの新しい小技をいくつか学びました。
GTKサブシステムのメンテナンスチーム全員が参加し、書き直しの完了に協力してくれたことも強調しておきたいと思います。彼らも実に多くの仕事をしてくれました。
書き直された新しいGhostty GTKアプリケーションは、現在、mainからGhosttyをソースビルドした場合のデフォルトになっています。また、数週間後にリリースされる1.2で、全ユーザーに提供される予定です。
脚注
「プラットフォームネイティブ」と言うと、Linux界隈の人たちは本気で熱くなります。Linuxにはそのようなものは存在しないからです。ただ、他のアプリケーションと比べれば、GTKアプリ(あるいはQtアプリ)のようなものは多くのデスクトップ環境で「ネイティブ」に感じられる、という点には、分別のある人なら同意してくれるでしょう。 ↩
記事をランダムに読む