1 ポイント 投稿者 GN⁺ 17 시간 전 | 1件のコメント | WhatsAppで共有
  • ISO C標準のみに従うコードはまれで、実際のCコードベースは機能追加やコンパイラ・ライブラリごとの差異を回避するために非標準拡張へ依存している
  • 実用的なCコンパイラは <stdio.h> のようなシステムヘッダから処理できる必要があるが、glibc__attribute__((packed))#include_next といったGNU拡張とその前提によって障壁を作っている
  • SDLのbyteswappingロジックは、ISAマクロがあれば inline assembly を選ぶことがあり、GCC・clang以外のコンパイラにもGCC式拡張が求められる場合がある
  • OpenBSDとGnulibの extern inline 処理は、C99とGCCの意味の違い、プラットフォーム別分岐、_FORTIFY_SOURCE 条件のため、inlineセマンティクス の互換性を複雑にしている
  • 小規模なCコンパイラは、upstreamパッチ、downstreamパッチ、専用ガードの確保、GCC互換のふり のいずれかを選ぶ必要があり、機能テストマクロの拡大がより良い方向に見える

glibcヘッダが作る最初の障壁

  • 実用的なCコンパイラになるには、システムCライブラリのヘッダをプリプロセスして構文解析できなければならず、<stdio.h> を処理できなければ hello world さえ通しにくい
  • GNU/Linux環境では、この障壁が glibc につながる
  • glibcは、ほぼすべてのlibcヘッダが間接的にインクルードする sys/cdefs.h で、コンパイラが事前定義したマクロを検査して対応している拡張を判定する
  • 非対応の拡張は関連定義を消す形で処理するが、この互換ロジック自体が実際には壊れることがある
  • struct epoll_event__attribute__((packed))

    • Linuxの sys/epoll.h にある struct epoll_event は、GNUの __attribute__((packed)) を使う packed struct である
    • この属性は64ビット環境で構造体レイアウトを変えるため、無視すると ABI が壊れる
    • コンパイラが __attribute__((packed)) を実装していても、それだけでは十分ではない
    • sys/cdefs.h には、GCC、clang、tcc 以外では __attribute__(xyz) を空マクロとして定義するコードがある
    • その結果、他のコンパイラはpacked属性をサポートしていても、glibcヘッダ内でその属性が取り除かれる可能性がある
    • epoll ヘッダはLinux専用なので、C標準の移植性基準をそのまま当てはめにくいという反論もありうる
  • limits.h#include_next

    • stddef.hstdint.hlimits.hfloat.h のような一部のCヘッダはfreestanding実装でも必要であり、コンパイラが提供しなければならない
    • POSIXは、標準C定数に加えてPOSIX専用定数も limits.h に定義することを要求するため、コンパイラの limits.h の上にプラットフォーム別 limits.h が必要になる
    • glibcの <limits.h> は、GNU Cでない場合はANSI limits.h の値を直接定義し、GCC環境では #include_next <limits.h> でコンパイラヘッダを取り込む
    • この構造は、GCC専用のbuiltin limits.h が特定マクロを定義すると仮定しており、#include_next 拡張にも依存している
    • clangもこの構造を回避処理 しなければならない

SDLの機能検出とinline assemblyの問題

  • SDL_endian.hbyteswapping関数 は、可能ならコンパイラbuiltinやinline assemblyを使い、最後の手段として通常のビット演算実装に置き換える
  • 検出ロジックはおおむね次の順で動く
    • GCCまたはclangで、__has_builtin(__builtin_bswapX) があればbuiltinを使う
    • MSVC 8.0以上ならMSVC intrinsic #pragma を使う
    • __x86_64__ のようなISA別マクロが定義されていればinline assemblyを使う
    • それ以外では通常のビット演算実装を使う
  • GCCやclangではないコンパイラが、妥当な理由でISA別のpredefined macroを定義していると、この順序が問題になる
  • そのコンパイラがbswap builtinと __has_builtin 特殊演算子を提供していても、ロジック上はGCC式inline assemblyを使おうとする可能性がある
  • 結果として、未知のコンパイラ に対してもGCCスタイルinline assemblyをサポートすると期待する構造になる

OpenBSD libcと extern inline の混乱

  • OpenBSDの一部ヘッダ には、最適化時にコンパイラが任意に使えるinline関数定義が含まれている
  • これらの関数は __only_inline マクロで定義され、コンパイラが実際にinline化しない場合は外部シンボルへ置き換える必要がある
  • つまり、extern linkageを持つinline関数が必要になる
  • C99 inlineとGCC inlineの意味の違い

    • inline はC99で規定されているが、標準の動作はC99以前の非標準なGCCの動作と衝突する
    • ヘッダ内のinline定義では、関数本体とともに extern inline を使う必要があり、この場合は実際のexported functionを生成しない
    • translation unitでは、関数定義をexportするために inline のみを付けて宣言しなければならない
    • inline の意味はC++とCでも異なる
    • この違いは Youtao Guoの記事 で詳しく扱われている
  • OpenBSDの __only_inline

    • OpenBSDはGCC inline semanticsに依存している
    • GCCのバージョン差を埋めるため、sys/cdefs.h__only_inline マクロは、新しいGCCでは明示的な __attribute__ によって旧来のgnu89 inline semanticsを指定する
    • 非GNUコンパイラでは __only_inlinestatic linkageとして定義される
    • その結果、関数が互いに衝突するlinkageで宣言・定義されて壊れることがある
  • _ANSI_LIBRARY による回避

    • OpenBSDは _ANSI_LIBRARY マクロを尊重する
    • このマクロを定義すると、signal.h のような標準ヘッダで壊れる __only_inline 定義の使用を完全に省略する
    • 最適化版は得られないが、少なくともビルドは動く
  • Gnulibの extern inline 互換コード

    • Gnulibextern inline 互換コードは、Guileやnanoをビルドするときにも現れる
    • extern-inline.m4 は、このCのコーナーケースにおける壊れた奇妙な実装を扱うため、複雑な条件分岐を含んでいる
    • 条件には、Apple、DragonFly、FreeBSD、GCC、clang、PCC、HP cc、PGI、SunPro C、_FORTIFY_SOURCE__GNUC_STDC_INLINE____GNUC_GNU_INLINE__ といった環境差が反映されている

Android bionicのclang前提

  • bionic はAndroidのlibcであり、そのヘッダはGCCよりも clang を強く前提にしている
  • bionicヘッダは、nullability checks のために _Nonnull_Null_unspecified のようなclang専用拡張を多用している
  • こうしたマクロは、コマンドラインフラグで #define して消すこと自体はそれほど難しくない
  • Termuxを通じてAndroidスマートフォンをネイティブなaarch64開発環境として使うとき、bionicヘッダでこの問題が表面化する
  • _Null_unspecified__BIONIC_COMPLICATED_NULLNESS とも呼ばれ、関連定義は bionicの sys/cdefs.h にある

小規模なCコンパイラが直面する選択肢

  • ISO C標準のみに従うコード は現実にはまれで、多くのCコードベースは非標準動作や言語拡張に依存している
  • こうした依存は、追加機能だけでなく、コンパイラやライブラリごとに異なるバグや欠落を回避する過程でも生まれる
  • 複数環境をサポートしようとするコードベースはプリプロセッサ検査やガードに依存するが、この方式は壊れやすく扱いにくい
  • antcc のようなCコンパイラを作ると、こうした互換性問題が繰り返し表面化する
  • 多くのオープンソースプロジェクトが、必須でもない場面でコンパイラ別の非標準拡張や動作に依存すると、代替コンパイラ側の対応負担は大きくなる
  • 同時に、すべての開発者に対して、小さくあまり知られていないコンパイラまで含めて複数コンパイラでCコードをテストするよう求めるのも難しい
  • Cの移植性は、それ自体で十分に難しい
  • コンパイラ作者の立場から見た可能な選択肢は4つある
    • 非互換性をupstreamにパッチしようと試みる
    • 十分に有名になって、開発者に専用の #ifdef 検査と基本テストを追加してもらう
    • downstreamで処理し、パッチ別個のパッチ を配布する
    • 特定バージョンのGCCのふりをして、その拡張を実装する
  • upstreamパッチは勝ち目の薄い戦いに見え、downstreamパッチが最も簡単な方法である
  • 多くのコードベースを、ユーザーと開発者に最小限の混乱でサポートするには、GCC互換のふり が現実的だが実装負担は大きい
  • clangはGCC 4.2.1互換を主張するために __GNUC__=4__GNUC_MINOR__=2__GNUC_PATCHLEVEL__=1 を定義している
  • clangは今では別個のサポート対象に近いが、Linux kernelをclangでコンパイルできるようにするには、両プロジェクトにパッチが必要だったほど大きな努力が要った

GCCマクロと追随問題

  • GCCのふりをする方式にも問題がある
  • 多くのコードベースが #ifdef __GNUC__ だけを検査し、バージョン確認なしで最新のGCC拡張を使うことがある
  • この場合、代替コンパイラは常に 追いかけ続ける 必要がある
  • clangが4.2.1より新しいGNU拡張をサポートしながらも __GNUC__ マクロ値を引き上げない理由の1つがここにある
  • 関連する背景はLLVMの __GNUC__ minorバージョン引き上げ議論 にある

より良い方向と現在の状況

  • 理想的には、コンパイラ別ガードやバージョン検査の代わりに 機能テストマクロ がもっと広く使われるべきである
  • 有用な機能テストマクロには __has_builtin__has_feature__has_attribute がある
  • 標準マクロである __STDC_NO_VLA__ のような方式も、さらに広く使えるはずだ
  • 現在の *NIX世界では、良くも悪くも GCC/clangの準二強体制 が基本状態になっている
  • 独立した小規模Cコンパイラの開発も続いている

1件のコメント

 
Lobste.rsのコメント
  • (kefirコンパイラの作者)<sys/cdefs.h>__attribute__ 問題は、経験上もっとも厄介な部類に入る。これのせいで epoll や一般的な packed 構造体、constructor、シンボル可視性が壊れるため、kefir には 次のモンキーパッチヘッダ を同梱することになった
    理想的ではないが、おそらく最も現実的な方法で、実際に外部テストスイートでカスタムパッチの大半を取り除けるようになった
    もう1つの失敗パターンは、バグのある代替コードだ。一部のプロジェクトはコンパイラを検出してそれに合わせて動こうとするが、代替コンパイラでのテスト不足のため、fallback コードがバグだらけだったり、まともに保守されていなかったりすることがある。コンパイラ作者の立場からすると、「未対応コンパイラ」として即座に失敗するよりずっと腹立たしい。たとえば、プログラムと事前コンパイル済みライブラリの間で整数 typedef の幅が食い違うような奇妙なオコンパイルを、自分で直接デバッグしなければならないからだ

    • ターミナルでも似たことが起きる。$TERMxterm-256color に設定して xterm のふりをしないと、いろいろなものが壊れてしまう
      どう解決すべきか本当にわからない。結局のところ、私たちのプロジェクトが十分に広まり、有名になる必要があるのだろうかと思ってしまう。簡単だね!
    • モンキーパッチヘッダ方式は slimccも使っている方法 のようで、かなり良い妥協策に見える
      コンパイラ検出 fallback がきちんと管理されていないせいで生じる 奇妙なオコンパイルも何度か経験した気がするし、本当に厄介だ
  • 主に linux-musl で cproc を開発していたので、glibc が別のコンパイラでは __attribute__ を無効化するとは知らなかったが、実際かなりひどい状況だ。コメントでは attribute の使用は無視されても問題ないと書かれているが、ほとんどのアプリケーションコードが sys/cdefs.h を間接的にインクルードし、無視してはいけない attribute を使う可能性があることを考慮していない
    packed のほかにも、alignedconstructor はよく使われる
    これがどこかの issue tracker に報告されているのか気になる。cdefs.h 内での attribute 使用の大半はすでに __glibc_has_attribute で保護されているようなので、包括的な __attribute__ 無効化で実際に何を達成しているのか、取り除けるのかも気になる
    libc ヘッダが使う機能のうち、コンパイラが対応有無をうまく示す方法がない場合も問題だ。__has_attribute__has_builtin のような形で表に出ない機能で、思いつく例は __asm__ ラベルだ。NetBSD はこれをシンボル名変更に使っていて、__GNUC____PCC__ がなければ #error を出す。ただ、未対応なら単に試して失敗させる以外に何を提案すべきかはわからない
    __builtin_va_list 関連の問題にも遭遇した。libc が __GNUC__ なしでは define va_list to void * と定義したり、さらには衝突する定義を置いたりすることがある。これも __has_builtin ではテストできない。__has_builtin(__builtin_va_arg) が十分良いテストになるかもしれないが、macOS でこれをどう直させるべきかはよくわからない

    • /usr/include/sys/usr/include/bits__attribute__ の使用をざっと探してみたところ、保護されていない使用が多かった。主に __format____aligned____noreturn__ なので、これらも修正が必要だろう
      glibc は全体として GCC 以外のコンパイラとの互換性を優先事項にしていないように見えるので、その種のパッチを受け入れてくれるかはわからない。今年初めのシステムアップグレード後、glibc が Linux ヘッダに保護なしの __SIZE_TYPE__ 使用を追加し、そのせいで私のコンパイラでは一部のプロジェクトをコンパイルできなくなった。報告はしたが、まだ修正されておらず、結局 GCC に合わせるため __X_TYPE__ 形式の事前定義マクロを追加した
      __asm__ ラベルの問題については、良い解決策があまり思い浮かばない。ただ、asm 名変更がそもそも動作に 100% 必要なら、コンパイラチェックをするより、単に試して失敗させるほうがよいかもしれない
      __builtin_va_list はかなり深刻だ。__has_builtin(__builtin_va_list) が動くはずだと思っていたが、apparently そうではない