C拡張、移植性、代替コンパイラについて
(lemon.rip)- 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標準の移植性基準をそのまま当てはめにくいという反論もありうる
- Linuxの
-
limits.hと#include_nextstddef.h、stdint.h、limits.h、float.hのような一部のCヘッダはfreestanding実装でも必要であり、コンパイラが提供しなければならない- POSIXは、標準C定数に加えてPOSIX専用定数も
limits.hに定義することを要求するため、コンパイラのlimits.hの上にプラットフォーム別limits.hが必要になる - glibcの
<limits.h>は、GNU Cでない場合はANSIlimits.hの値を直接定義し、GCC環境では#include_next <limits.h>でコンパイラヘッダを取り込む - この構造は、GCC専用のbuiltin
limits.hが特定マクロを定義すると仮定しており、#include_next拡張にも依存している - clangもこの構造を回避処理 しなければならない
SDLの機能検出とinline assemblyの問題
SDL_endian.hの byteswapping関数 は、可能ならコンパイラbuiltinやinline assemblyを使い、最後の手段として通常のビット演算実装に置き換える- 検出ロジックはおおむね次の順で動く
- GCCまたはclangで、
__has_builtin(__builtin_bswapX)があればbuiltinを使う - MSVC 8.0以上ならMSVC intrinsic
#pragmaを使う __x86_64__のようなISA別マクロが定義されていればinline assemblyを使う- それ以外では通常のビット演算実装を使う
- GCCまたはclangで、
- 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_inlineがstaticlinkageとして定義される - その結果、関数が互いに衝突するlinkageで宣言・定義されて壊れることがある
-
_ANSI_LIBRARYによる回避- OpenBSDは
_ANSI_LIBRARYマクロを尊重する - このマクロを定義すると、
signal.hのような標準ヘッダで壊れる__only_inline定義の使用を完全に省略する - 最適化版は得られないが、少なくともビルドは動く
- OpenBSDは
-
Gnulibの
extern inline互換コード- Gnulib の
extern 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__といった環境差が反映されている
- Gnulib の
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パッチは勝ち目の薄い戦いに見え、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バージョン引き上げ議論 にある
1件のコメント
Lobste.rsのコメント
(kefirコンパイラの作者)
<sys/cdefs.h>の__attribute__問題は、経験上もっとも厄介な部類に入る。これのせいで epoll や一般的な packed 構造体、constructor、シンボル可視性が壊れるため、kefir には 次のモンキーパッチヘッダ を同梱することになった理想的ではないが、おそらく最も現実的な方法で、実際に外部テストスイートでカスタムパッチの大半を取り除けるようになった
もう1つの失敗パターンは、バグのある代替コードだ。一部のプロジェクトはコンパイラを検出してそれに合わせて動こうとするが、代替コンパイラでのテスト不足のため、fallback コードがバグだらけだったり、まともに保守されていなかったりすることがある。コンパイラ作者の立場からすると、「未対応コンパイラ」として即座に失敗するよりずっと腹立たしい。たとえば、プログラムと事前コンパイル済みライブラリの間で整数 typedef の幅が食い違うような奇妙なオコンパイルを、自分で直接デバッグしなければならないからだ
$TERMをxterm-256colorに設定して xterm のふりをしないと、いろいろなものが壊れてしまうどう解決すべきか本当にわからない。結局のところ、私たちのプロジェクトが十分に広まり、有名になる必要があるのだろうかと思ってしまう。簡単だね!
コンパイラ検出 fallback がきちんと管理されていないせいで生じる 奇妙なオコンパイルも何度か経験した気がするし、本当に厄介だ
主に linux-musl で cproc を開発していたので、glibc が別のコンパイラでは
__attribute__を無効化するとは知らなかったが、実際かなりひどい状況だ。コメントでは attribute の使用は無視されても問題ないと書かれているが、ほとんどのアプリケーションコードがsys/cdefs.hを間接的にインクルードし、無視してはいけない attribute を使う可能性があることを考慮していないpackedのほかにも、alignedとconstructorはよく使われるこれがどこかの issue tracker に報告されているのか気になる。cdefs.h 内での attribute 使用の大半はすでに
__glibc_has_attributeで保護されているようなので、包括的な__attribute__無効化で実際に何を達成しているのか、取り除けるのかも気になるlibc ヘッダが使う機能のうち、コンパイラが対応有無をうまく示す方法がない場合も問題だ。
__has_attributeや__has_builtinのような形で表に出ない機能で、思いつく例は__asm__ラベルだ。NetBSD はこれをシンボル名変更に使っていて、__GNUC__や__PCC__がなければ#errorを出す。ただ、未対応なら単に試して失敗させる以外に何を提案すべきかはわからない__builtin_va_list関連の問題にも遭遇した。libc が__GNUC__なしでは defineva_listtovoid *と定義したり、さらには衝突する定義を置いたりすることがある。これも__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 そうではない