シリーズ: AI とともにコードを学ぶ

Linuxで18個のDLLすべてがグリーンに

Linuxのビルドをグリーンに変えたシンボル可視性の一斉修正。

Linuxで18個すべてがグリーンに 347個の関数にタグ付け、1つのCIガード、3つのプラットフォーム libraku_01 libraku_02 libraku_03 libraku_04 libraku_05 libraku_06 libraku_07 libraku_08 libraku_09 libraku_10 libraku_11 libraku_12 libraku_13 libraku_14 libraku_15 libraku_16 libraku_17 libraku_18 RAKU_API __attribute__((visibility("default"))) nm -D ガード:シンボルを一つでも落とすPRを失敗させる
18個のライブラリ、一貫した可視性の契約、それを正直に保つガード。

クロスプラットフォームビルドは、単にリーチを広げるだけではない。コードベースが、特定のコンパイラの静かな計らいに頼るのをやめざるを得なくする。プラットフォームを追加するたびに、すでに持っていたプラットフォーム上でもランタイムはより正直になる。

Linuxのビルドは、何週間もの間、既知の部分的成功状態だった。18個のネイティブランタイムDLL(このエンジンは、焦点を絞った共有ライブラリ群として構成されている)のほとんどは、クリーンにビルドされていた。いくつかはそうではなかった。失敗のパターンは毎回同じ形をしていた。コードベースに明らかに存在する関数について、リンク時にundefined-symbolエラーが出るというものだ。C++でLinuxの共有ライブラリを扱ったことがある人なら、この先の展開はお見通しだろう。

この土曜日、私はこの作業を終わらせるつもりで取り組んだ。土曜日が終わる頃には、18個のDLLすべてがクリーンにビルドされ、Linux CIパイプラインは完全なグリーンを報告していた。

この投稿は、実際の問題が何だったのか、修正がどのようなものだったのか、そしてなぜ3つのプラットフォーム(Linux、macOS、Windows MSVC)でビルドすることが対価を払うに値する規律だと考えているのか、についてのものだ。

失敗はどのように見えたか

パターンは常にこのようなものだった。例えば libraku_runtime.so にリンクするテストバイナリが、次のようなエラーでリンクに失敗する。

undefined reference to `raku_runtime_create_session(...)'
undefined reference to `raku_runtime_initialize(...)'
undefined reference to `raku_runtime_shutdown(...)'

関数は確かに存在していた。書かれていた。ソースファイルの中にあった。コンパイルもされていた。オブジェクトファイルにも含まれていた。だが、nm -D libraku_runtime.so で調べてみると、共有ライブラリはそれらをエクスポートしていなかった。

これはLinuxの共有ライブラリにおける典型的なシンボル可視性の問題だ。このランタイムは、ソースファイルに対してCMakeのフラグ -fvisibility=hidden を付けてビルドされていた。これはこれで、まったく問題のないデフォルト設定だ。-fvisibility=hidden の意図は、内部シンボルを共有ライブラリの公開シンボルテーブルから排除することにある。公開する必要があるものは何であれ、__attribute__((visibility("default"))) か、それに展開されるマクロで明示的にタグ付けしなければならない。

ランタイムのビルドシステムには、各プラットフォームで正しい可視性属性に展開されるはずの RAKU_API というマクロがあった。Windowsでは、RAKU_API はDLLをビルドするときには __declspec(dllexport) に、それを利用する側では __declspec(dllimport) に展開されていた。Linuxでは、ビルド時には __attribute__((visibility("default"))) に、利用側では何も展開しないはずだった。

問題は、このマクロのLinux側が、すべての公開関数に適用されていなかったことだった。タグ付けされている関数もあれば、そうでない関数も多くあった。タグ付けされていない関数は -fvisibility=hidden のデフォルト設定によって隠され、公開シンボルテーブルから消えていた。

修正はどのようなものだったか

3つのステップ、すべて機械的で、すべて書き留める価値がある。

一つ目:公開APIのすべてのヘッダーを監査し、RAKU_API タグの欠落を洗い出す。 すべての公開APIの .h ファイルをパースし、公開されるべきなのにタグ付けされていない関数宣言をすべて見つけるスクリプトを書いた。このスクリプトは、関数名、ファイル、行番号を報告する。スクリプトを実行した。18個のDLLにまたがって、タグが必要な関数が347個というリストが得られた。

二つ目:タグを一括で追加する。 これはまさに、エージェントが得意とする種類の機械的なリファクタリングだ。イシューの説明はこうだった。「以下にリストされた各関数について、宣言行に RAKU_API を追加せよ。関数本体は変更しないこと。他の行は一切変更しないこと。各サブシステムのバッチの後にビルドを実行し、Linux上でまだビルドできることを確認せよ」。エージェントはこれをサブシステムごとに、きれいにやり遂げた。各サブシステムのPRはそれ単体でレビュー可能で、マージするたびにLinuxのビルドはよりグリーンになっていった。

三つ目:リグレッションを防ぐCIチェック。 PR時に実行されるチェックを追加した。このチェックはランタイムをLinux上でビルドし、生成された各 .so に対して nm -D を実行し、エクスポートされたシンボルのリストを公開APIヘッダーで宣言されている期待値のセットと比較し、期待されるシンボルが一つでも欠けていればPRを失敗させる。これは、「新しい関数にタグを付け忘れた」という次のインスタンスを、何カ月も後ではなく、それが発生したその日のうちに捕まえてくれる種類のガードレールだ。

土曜日が終わる頃には、18個のDLLすべてが公開表層の全体をエクスポートしていた。テストバイナリはリンクできた。テストは実行できた。Linuxのビルドはグリーンになった。

なぜ3つのプラットフォームに対価を払う価値があるのか

当然浮かぶ疑問がある。そもそもなぜLinuxにわざわざ対応するのか。このランタイムが対象とするのはARグラスであり、それは開発者のマシンにおけるLinuxでもWindowsでもmacOSでもない、専用のOSを動かしている。実際のターゲットハードウェアをサポートするコストに加えて、なぜクロスプラットフォームのコストまで払うのか。

理由は3つある。

一つ目:サーバーサイドのAI推論とクラウドレンダリングはLinux上で行われる。 ARエクスペリエンスのクラウド側のコンポーネント(モデルサービング、ワールドステートの同期、永続化)は何であれLinux上で動いている。ランタイムには、クラウド側が呼び出すフックがある。それらのフックは、クラウド側が統合できるように、Linux上でビルドされ、動作しなければならない。もしランタイムがWindows専用の代物であれば、クラウド側は別個の通信シムを構築するか、Linux互換レイヤーの下でランタイムを動かすかのどちらかを強いられる。どちらも、パートナーにやってほしいことではない。

二つ目:Linux上のCIはWindows上のCIより速く安い。 私がマージするすべてのPRはCIを通る。Linux CIランナーは、Windows CIランナーより小さく、速く、安い。CIループが速いほど、エージェントと私が土曜日の間に回せるイテレーションの数は増える。第一級のビルドターゲットとしてのLinuxは、開発ワークフロー全体を加速させる。

三つ目:クロスプラットフォームの規律はバグを捕まえる。 これが最も根深い理由だ。コードベースが1つのプラットフォームでしかビルドされない場合、開発者が手を伸ばすパターンは、そのプラットフォームで動くパターンになる。クロスプラットフォームのビルドは、そのパターンを移植可能なものにすることを強いる。暗黙的な可視性ではなく明示的な可視性属性、「このコンパイラではlongは32ビット」ではなく明示的なクロスプラットフォームの型幅、「Windowsでは動く」ではなく明示的なスレッディングセマンティクス。今回のシンボル可視性の修正は、まさにそのパターンだ。この修正によって可視性の契約が明示的になったからこそ、コードベースはあらゆるプラットフォームにおいて、修正前より強くなった。

これが一般化すること

いくつかの正直なパターンがある。

Linuxにおけるシンボル可視性は、誰もが一度は払う税金だ。 プロジェクトが最初にこの問題にぶつかったとき、それは謎めいていて苛立たしいものに感じられる。(RAKU_API マクロを一貫して適用し、CIガードを添えるという)修正が一度整ってしまえば、それは目に見えなくなる。コストがかかるのは最初の一回だけだ。早めに払っておこう。

シンボル可視性のリグレッションに対するCIガードは、オプションではない。 まだ誰も使っていない関数がタグを欠いているせいで、表面化するまでに何カ月もかかるようなバグは、まさにCIガードがそれが着地したその日に捕まえてくれる種類のバグだ。ガードを設定しよう。

クロスプラットフォームビルドは、コードベースをより正直にする。 コードベースがどこかで一つのプラットフォームの暗黙的な振る舞いに依存している場合、クロスプラットフォームへの移植は、その振る舞いを明示的にすることを強いる。私がコードベースを新しいプラットフォームに移植するたびに、その新しいプラットフォームは、元のプラットフォームが静かに吸収していたバグを表面化させてきた。その修正は、新しいプラットフォームだけでなく、あらゆるプラットフォームにおいて改善となる。

パートナーとビルダーがここから得るべきこと

もしあなたが、クロスプラットフォームのAR製品向けにどのエンジンの上に構築するか決めようとしているパートナーなら、そのチームのビルドマトリクスがどうなっているか尋ねてみるといい。Linux、macOS、Windowsでビルドしているチームは、そのコードベースが3つのプラットフォーム間の違いによって鍛えられているチームだ。1つのプラットフォームだけでビルドしているチームは、そうではないチームだ。

もしあなたが自分自身のクロスプラットフォーム共有ライブラリプロジェクトに取り組んでいる開発者なら、シンボル可視性の監査は、診断に3日かかるような理由でテストが失敗し始める3月にではなく、今日実行すべき監査だ。共有ライブラリに対して nm -D を実行しよう。ヘッダーと比較しよう。その食い違いこそが監査だ。

もしあなたが、コーディングエージェントがクロスプラットフォームのネイティブコードを書くAIラボなら、可視性属性のマクロは、そのエージェントの実務知識に必要な種類のものだ。新しい公開関数を書いてタグ付けを忘れるエージェントは、後追いのPRというコストをあなたに負わせるエージェントだ。すべての公開関数に一貫してタグを付けるエージェントは、それ自体で元が取れるエージェントだ。

土曜日のまとめ。18個のDLLがLinuxでグリーンになった。CIは今や速くなった。コードベースはより正直になった。ビルドマトリクスは3プラットフォーム分の幅になった。

構築に戻ろう。

触れるすべてのプラットフォームによって鍛えられたランタイム

RakuAIは、ARグラス、クラウド推論、そしてその間のすべてに対応するクロスプラットフォームの空間ランタイムだ。Linux、macOS、Windows、すべてグリーン。パートナーが移植可能な基盤の上に構築する理由を見てほしい。

← すべての記事