
2026/05/25 23:15
C エクステンション、ポータビリティ、代替コンパイラ
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
主要な知見は、現代のソフトウェア開発において公式の ISO C 標準を完全に遵守することは稀で実用的ではないという点である。現実の世界でのプログラミングでは、重大なバグと制限に対処するために非標準的な動作やコンパイラの拡張に大いに依存しており、これらの依存関係を無視するとアプリケーションバイナリーインターフェース (ABI) が壊れてしまう。例えば、glibc のヘッダーなどの主要コンポーネントには互換性のチェックが壊れており、GCC 固有のマクロに依存している。また、
sys/epoll.h などはパケット構造体アトリビュートを使用し、64 ビットシステムではメモリ配置を変えてしまう。主要なオペレーティングシステムはこの断片化をさらに悪化させている:Android の bionic ライブラリは Clang の使用と特定の拡張を前提としており、OpenBSD は __only_inline などの GCC 固有のマクロに依存している。さらに、POSIX 標準は ISO で定義されていない定数を処理するために複雑なインクルード(例えば #include_next)を必要とする。また、コンパイラ固有のバUILT-IN ヘッダーは各ツールチェーンで異なるパスに存在し、統合された ISO 標準への移行を試みると重大なリンカー競合に直面する。独立した C コンパイラーが存在する(tcc, cproc, scc, vbcc, nwcc, kefir, slimcc を含む)ものの、バージョンチェックやセキュリティフラグを含むプリプロセッサロジックへの深い依存は、業界全体における真のポータビリティを実際に阻害している。本文
C コンパイラ開発における現実と非標準コードの問題点
C を扱った経験がある方であればお分かりと思いますが、完全に ISO C 標準に準拠したコードを書くのは現実的には極めて困難で、実際には稀です。世に出ている実用的な C コードの多くは、以下の理由から非標準的な挙動や言語拡張機能を利用しています。
- バグ回避: 異なるコンパイラやライブラリの制限を回避するため。
- 環境対応: プリプロセサチェックやガードを用いて複数の環境への対応を試みるため(しかし最善でも不安定になり、最悪の場合は壊れている)。
筆者は C コンパイラ開発の過程でこの状況を頻繁に遭遇してきました。以下に主要な事例を挙げます。
1. glibc (GNU C ライブラリ)
システム用 C ライブラリのヘッダは、有用なコンパイラを目指す者にとって最初の「障壁」です。
<stdio.h> をプリプロセスできないと、「Hello, World!」すら動作しません。glibc の可贵な点は、非 GCC コンパイラでもヘッダの互換性を維持しようとする努力にあることです。
問題点:sys/cdefs.h と間接的依存
は全ての libc ヘッダから間接的に包含される巨大なファイルです。sys/cdefs.h- この中でコンパイラで定義済みマクロを確認し、対応していない拡張機能については**無効化(
で塞ぐ)**処理が施されています。#define - しかし、この仕組み自体が壊れることがあります。
具体的な例:epoll.h と ABI 破綻
- Linux の
にあるsys/epoll.h
はパacked 構造体であり、GNU のstruct epoll_event
を使用しています。__attribute__((packed)) - 64 ビット環境ではこれを無視すると構造体の配列が変化するため、ABI が破綻します。
- コンパイラ側でサポートを追加しても不十分です。なぜなら、前述の
に以下のコードが含まれているためです。sys/cdefs.h
/* GCC、clang、互換コンパイラは '__attribute__' 構文を使って 様々な有用な宣言を行えるが、理解できないコンパイラでは これらを省略しても問題ない。 */ #if !(defined __GNUC__ || defined __clang__ || defined __TINYC__) # define __attribute__(xyz) /* Ignore */ #endif
- 結論: GCC、clang、あるいは tcc でなければお手上げです。
- 補足: POSIX 標準では
が C 標準の定数に加えて、POSIX 固有の定数も定義することを要求しており、コンパイラ側のヘッダの上層にプラットフォーム固有のlimits.h
を追加する必要があります。limits.h
limits.h の複雑な依存関係
glibc の
<limits.h> は以下の形式で構成されています。
... /* GNU CC でなければすべて自分で記述しなければなりません。 他方、GCC の定義は次のように使います。 */ #if !defined __GNUC__ || __GNUC__ < 2 /* #include_next が使えないため、ANSI 仕様の <limits.h> を標準の 32 ビット単語用に定義します。 これらは 8 ビット 'char'、16 ビット 'short int'、および 32 ビット 'int' と 'long int' を前提としています。 */ # define CHAR_BIT 8 ... # endif /* limits.h */ #endif /* GCC 2. */ #endif /* !_LIBC_LIMITS_H_ */ /* GCC の <limits.h> を読み込み、ほぼすべての ISO 定数を定義します。 この #include_next を重複包含チェックの外に置くのは、このファイルを複数回 include しても GCC のヘッダから定義を取得できる必要があるためです。 */ #if defined __GNUC__ && !defined _GCC_LIMITS_H_ /* '_GCC_LIMITS_H_' は GCC のファイルが定義するマクロです。 */ # include_next <limits.h> #endif /* 一部の GCC バージョンの <limits.h> では LLONG_MIN、LLONG_MAX、ULLONG_MAX が定義されていません。 */ #if defined __USE_ISOC99 && defined __GNUC__ # ifndef LLONG_MIN # define LLONG_MIN (-LLONG_MAX-1) # endif ... #endif #ifdef __USE_POSIX /* POSIX では <limits.h> に追加の項目が定義されます。 */ # include <bits/posix1_lim.h> #endif ...
- このコードは、
拡張機能の使用に加え、GCC 固有のビルトイン#include_next
に依存して一部のマクロを正しく定義する必要があります。limits.h - clang でも同様の迂回策を採らざるを得ない状況です。
2. SDL
SDL_endian.h はバイト交換関数の機能検出に奇妙なロジックを採用しています。その目的は、可能であればコンパイラ固有のビルトインやインラインアセンブリを使用し、万が一の場合のみ汎用的なビット演算 (recourse) することです。
実装における判断フロー
実際のコードでは以下の順序で判断されています。
- ビルトイン存在確認: (GCC または clang で)
が存在すれば → ビルトインを使用__has_builtin(__builtin_bswapX) - MSVC 特定: それ以外で(MSVC ≥ v8.0 の場合)→ MSVC 固有インライン指令
を使用#pragma - ISA 特定マクロ: それ以外で ISA 特定マクロ(例:
)が定義されていれば → インラインアセンブリを使用__x86_64__ - フォールバック: その他 → 汎用的なビット演算による実装を使用
- 問題点: GCC でも clang でもなく、ISA 特定のマクロを正当な理由で定義している場合でも、ビルトインが存在し、かつ
オペレータも利用可能な状況であっても、拡張されたインラインアセンブリを試みるのです。__has_builtin - 未知のコンパイラが GCC スタイルの拡張型インラインアセンブリをサポートすることを期待するのは奇妙です。
3. OpenBSD libc
OpenBSD のいくつかのヘッダには、コンパイラ最適化時にオプションで使用するよう意図されたインライン関数定義が含まれています。これらは
__only_inline マクロを用いて定義されており、例えば:
__only_inline int sigemptyset(sigset_t *__set) { *__set = 0; return (0); }
セマンティクスに関する混乱
- 意図: コンパイラが実際にはインライン化しない場合に外部シンボルへのフォールバックを行うよう意図されています(「extern リンケージを持つインライン関数」)。
- 問題: C99 規格と、GCC の C99 以前の非標準な挙動(4.2 以前までのデフォルト)が衝突し、全体として混乱を招く実装になっています。
- ヘッダ内のインライン定義には
を使い、関数本体を記述し、これによって実際のエクスポート済み関数を生成しないようにしつつ、翻訳ユニット内では単純なextern inline
で関数を宣言してその定義をエクスポートするのが正しいはずです。inline - さらに混乱を招くのは、「inline」の意味が C++ と C では異なることです。
- ヘッダ内のインライン定義には
GCC 依存と互換性問題
- OpenBSD は GCC の inline セマンティクスに依存しており、GCC バージョン間の差を埋めるために
のsys/cdefs.h
マクロでは、新しい GCC バージョンに対して明示的な__only_inline
を使用して古式の GNU89 形式の inline セマンティクスを指定しています。__attribute__ - 一方、非 GNU コンパイラでは
リンケージとして定義され、結果的に矛盾するリンケージを持つ関数を宣言・定義してしまうため壊れてしまいます。static
回避策と Gnulib
マクロを定義すれば標準ヘッダ(例:_ANSI_LIBRARY
)内のこれらの壊れたsignal.h
定義が完全に無視され、動作します(おそらく大きな差はありません)。__only_inline- Guile や nano のビルド中に Gnulib の
互換コードにも遭遇しました。これは C のこの隅々たるケースの破綻した・奇妙な実装を浮き彫りにしています。extern inline
#if (((defined __APPLE__ && defined __MACH__) \ || defined __DragonFly__ || defined __FreeBSD__) \ && (defined HAVE___HEADER_INLINE \ ? (defined __cplusplus && defined __GNUC_STDC_INLINE__ \ && ! defined __clang__) \ : ((! defined _DONT_USE_CTYPE_INLINE_ \ && (defined __GNUC__ || defined __cplusplus)) \ || (defined _FORTIFY_SOURCE && 0 < _FORTIFY_SOURCE \ && defined __GNUC__ && ! defined __cplusplus)))) # define _GL_EXTERN_INLINE_STDHEADER_BUG #endif #if ((__GNUC__ \ ? (defined __GNUC_STDC_INLINE__ && __GNUC_STDC_INLINE__ \ && !defined __PCC__) \ : (199901L <= __STDC_VERSION__ \ && !defined __HP_cc \ && !defined __PGI \ && !(defined __SUNPRO_C && __STDC__))) \ && !defined _GL_EXTERN_INLINE_STDHEADER_BUG) # define _GL_INLINE inline # define _GL_EXTERN_INLINE extern inline # define _GL_EXTERN_INLINE_IN_USE #elif (2 < __GNUC__ + (7 <= __GNUC_MINOR__) && !defined __STRICT_ANSI__ \ && !defined __PCC__ \ && !defined _GL_EXTERN_INLINE_STDHEADER_BUG) # if defined __GNUC_GNU_INLINE__ && __GNUC_GNU_INLINE__ /* __gnu_inline__ は GCC 4.2 の診断を抑制します。 */ # define _GL_INLINE extern inline __attribute__ ((__gnu_inline__)) # else # define _GL_INLINE extern inline # endif # define _GL_EXTERN_INLINE extern # define _GL_EXTERN_INLINE_IN_USE #else # define _GL_INLINE _GL_UNUSED static # define _GL_EXTERN_INLINE _GL_UNUSED static #endif
4. bionic (Android libc)
bionic は Android の libc です。一転してヘッダが GCC よりも clang を前提しているのが大きな特徴です。
- NULL 性チェックなどに
、_Nonnull
などの clang 固有の拡張機能を大量に使用しており、他にも多数あります。_Null_unspecified1 - 対応策: コマンドラインフラグでこれらの定義を
して無効化するだけで対応可能です。#define
遭遇経緯
- 筆者がこれに遭遇したのは、Termux をネイティブな aarch64 開発環境として Android スマホで利用していたからです(笑)。
- その際に bionic のヘッダが使われます。
結論と展望
多くのオープンソースプロジェクトが、必須ではないことのためにコンパイラ固有の非標準拡張や挙動に依存するのは非常に面倒です。しかし同時に、あらゆる開発者が自らの C コードを複数のコンパイラ(特にマイナーまたは小規模なものを含む)でテストすることを求めるのも不公平だと言えます。C への互換性はもともと難しいものです。
コンパイラ開発者への提言
以下の解決策が考えられます:
- 上流側での修正: 不具合の修正を試みる(勝てる戦ではない可能性が高い)。
- 流行り: 自らのコンパイラに対してデフォルトで専用の
チェックとテストを追加できるよう、十分な人気を得る。#ifdef - 下流側での対応: パッチを配布する(最も容易)。
- 偽装実装: GCC の一部のバージョンを装ってその拡張機能を実装する。
- (4) 的手法の例: clang は GCC 4.2.1 と互換性を持つと主張するために
(そして__GNUC__=4
、__GNUC_MINOR__=2
)を定義しています。__GNUC_PATCHLEVEL__=1 - 問題点: 多くのコードベースが
をチェックし、そのマクロが定義されていればあらゆる最新の GCC 拡張機能を自由に使用します(バージョンチェックなし)。そのため追いかけっこを続けることになり、clang が新しい GNU 拡張機能をサポートしているにもかかわらず#ifdef __GNUC__
マクロを更新しない一因となっています。__GNUC__
理想とする状態
- 機能テストマacro (
,__has_builtin
,__has_feature
) や標準マクロ (__has_attribute
) が広く採用され、コンパイラ固有のガードやバージョンチェックに代わるべきです。__STDC_NO_VLA__
現時点では、良いも悪しきものの NIX 世界での地位は GCC/clang の準独占状態になっています。小さな独立系 C コンパイラ開発者の方々(tcc, cproc, scc, vbcc, nwcc, kefir など)には拍手を送りたいと思います。
※ 詳細は lobsters での議論や slimcc の開発者による事例を参照してください。