ISO Cの幻想:移植性、拡張性、およびコンパイラの苦闘

C言語の記述に多大な時間を費やしてきた人にとって、ISO C標準は実用的な現実というよりも、理論的な理想として捉えられることが多い。標準は移植性のためのベースラインを提供するが、現実世界のCコードベースの大部分は、パフォーマンスの向上、ハードウェアとの対話、あるいは単に既存のツールチェーンのバグを回避するために、非標準の動作、コンパイラ拡張、およびプラットフォーム固有のハックに依存している。

この緊張関係は、代替Cコンパイラを開発する者にとって、気が遠くなるような環境を作り出している。有用なコンパイラであるためには、単にISO標準を遵守するだけでは不十分であり、少数の支配的なコンパイラを優遇するプリプロセッサ・ガードや暗黙の前提条件という地雷原を切り抜けていかなければならない。

ゲートキーパー:glibcとシステムヘッダー

新しいCコンパイラにとっての最初のハードルは、システムのCライブラリ・ヘッダーである。GNU/Linuxにおいては、これはglibcを扱うことを意味する。glibcは非GCCコンパイラとの互換性を維持しようと試みるが、その実装はしばしば脆弱である。

顕著な例の一つは、sys/epoll.hにおけるstruct epoll_eventでの__attribute__((packed))の使用である。この属性は64ビットシステムにおける構造体のメモリレイアウトを変更するため、ABI互換性のために不可欠である。しかし、glibcsys/cdefs.hは、コンパイラがGCC、Clang、またはTCCとして識別されない場合、しばしば__attribute__(xyz)を空のマクロとして定義する:

#if !(defined __GNUC__ || defined __clang__ || defined __TINYC__)
# define __attribute__(xyz)     /* Ignore */
#endif

もしあなたのコンパイラがそのリストに含まれていない場合、packed属性は無視され、構造体のレイアウトが変わり、結果として生成されたバイナリはABIを破壊する。これは、システム的な問題、すなわち、移植性は特定の機能のチェックではなく、特定のコンパイラ名のチェックによって制限されていることが多いということを浮き彫りにしている。

インライン関数の複雑さ

インライン関数は、もう一つの大きな摩擦が生じている領域である。C99標準以前の非標準的なGCCの動作からC99標準への移行は、inlineextern inlineの扱いに分裂を生じさせた。

OpenBSDのlibcヘッダーは、関数の最適化されたバージョンを提供するために__only_inlineというマクロを使用している。非GNUコンパイラでは、これはしばしばstaticリンケージとしてデフォルト設定され、それがリンケージの競合エラーを引き起こすことがある。OpenBSDは、これらの定義を省略するために_ANSI_LIBRARYマクロを提供しているが、それは開発者に基本的なコンパイルを通すために最適化を犠牲にすることを強いる。

この断片化は非常に深刻であり、Gnulibのようなプロジェクトは、GCCの正確なバージョンや特定のOS(Apple、FreeBSD、DragonFly)を検出し、extern inlineを使用するかstaticリンケージを使用するかを決定するために、膨大で複雑なプリプロセッサ・ブロックを開発している。コミュニティの議論に見られるように、この「nutburger nonsense(ナンセンスな混乱)」は、C互換性のあるフロントエンドを実装しようとする者にとって共通の苦痛点である。

プラットフォーム固有の前提条件:AndroidとSDL

コアとなるlibcを超えて、サードパーティ製ライブラリやプラットフォーム固有の実装が、状況をさらに複雑にしている:

  • AndroidのBionic: Linuxベースのシステムの多くとは異なり、BionicはClangの使用を強く前提としている。それは、nullabilityチェックのための_Nonnull_Null_unspecifiedといったClang固有の拡張機能で溢れている。これらは#definedによって無効化できるが、これは「Cエコシステム」の中でさえ、唯一の真実のソース(single source of truth)が存在しないことを示している。
  • SDL: SDL_endian.hヘッダーは、バイトスワップのための階層的な検出システムを使用している。コンパイラがGCCまたはClangでない場合、コンtaielline assembly(インラインアセンブリ)が、コンパイラが組み込み関数(builtins)を提供している場合であっても、ISAマクロ(__x86_64__など)に基づいた拡張インラインアセンブリにフォールバックすることがある。これは、x86_64をターゲットとするいかなるコンパイラも、GCCスタイルのインラインアセンブリをサポートしていなければならないという前提に基づいている。

コンパイラ開発者のジレンマ

独立したCコンパイラを構築する場合、開発者は一般的に、これらの不整合を扱うために4つの選択肢に直面する:

  1. アップストリームへのパッチ適用: オリジナルのライブラリの修正を試みる。これは、膨大なコード量と、保守担当者が不明瞭なコンパイラへのサポートを追加することへの抵抗感のため、しばしば敗北する戦いとなる。
  2. 普及: ユーザーベースを拡大し、開発者が新しいコンパイラへの明示的なサポートを追加する動機を得られるようにする。
  3. ダウンストリームでのパッチ適用: コンパイラがサポートしようとしているライブラリに対してパッチを配布する。 これは短期的には最も簡単な解決策だが、拡張性がない。
  4. 「なりすまし」戦略: 特定のGCCのバージョンをであるかのように装う。

この4番目の選択肢は、広範な互換性を得るための最も現実的な道である。例えば、Clangは、GCC 4.2.1との互換性を主張するために__GNUC__=4を定義している。これにより、Clangは、最新かつ、おそらくサポートされていないGNU拡張機能のトリガーを避けることなく、ほとんどの#ifdef __GNUC__チェックを通過させることができる。しかし、これは、継続的な「追いかけっこ」を強きいる、コンパイラがその特定のGCCバージョンがコードベースが期待するすべての拡張機能を実装しなければならないという状況を生み出す。

より良い標準への道

現在のCの移植性は、GCC/Clangの二重占有(duopoly)による結果である。コミュニティのコン意センサスは、前進するための道は、コンパイラ名のガードではなく、機能テストマクロ(例:__has_builtin, __has_attribute)にあることを示唆している。コンパイラに対して、そのアイデンティティではなく、特定の能力を問い合わせることで、コードベースは、必要な機能がサポートされている実装のあらゆる実装において、真に移植可能になるだろう。

それまで、移植性の確保は、支配的なツールチェーンの前提条件をリバースエンジニアリングすることという困難な作業であり続け、真に標準に準拠拠することがCのエコシステムを、非実用的な稀少なものにしている。

Sources