
2026/02/09 3:02
「デス・オブ・ドゥームの小さなボール」 (2025)
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Fedoraの各リリースで行われた大規模再構築により、chocolate‑doom パッケージでビルド失敗が発見されました。GCC 15 のデフォルトオプション
-std=gnu23 は C23 キーワード false と true を組み込みのブール型として扱いますが、パッケージでは独自に { false, true } という列挙型を定義しているため衝突します。その結果、コンパイラは「keyword ‘false’ cannot be used as enumeration constant」とエラーを出し、ビルドが失敗します。
これを解決するために、
src/doomtype.h を修正するパッチが適用されました。このパッチでは C23 でコンパイルされる際にコンパイラの組み込み bool 型を使用するようにし、|| (__STDC_VERSION__ >= 202311L) をプリプロセッサガードに追加しました。これにより、C++ と C23 の両方で typedef bool boolean; が選択され、古い標準では元の列挙型定義が保持されます。その後、上流メンテナは「このプロジェクトは C99 用に書かれたもの」と宣言し <stdbool.h> をインクルードし、後方互換性を保つため typedef int boolean; を残すことにしました。
バグは実行時にも現れました。
<stdbool.h> から提供される _Bool に 255 のような範囲外の値が与えられると未定義動作が発生します—UBSan は無効な _Bool のロードを報告します。最小再現テスト(booltest.c)では、そのような値が真と偽の両方として評価され、chocolate‑doom 内で sprtemp[frame].rotate == false を比較する際にクラッシュにつながります。
パッチが上流化されたことで、Fedora は将来のリリースで修正済みパッケージを配布し、ビルド失敗と実行時クラッシュを排除します。メンテナは他のパッケージでも同様の問題を防ぐために言語標準のデフォルト設定に注意を払うべきです。
Text to translate
(Original text omitted for brevity)
本文
私は古典的な DOOM に弱いということを告白しよう
31 年も経った今でも、ゲーム自体(正直言って私はプレイが苦手ですが)や他人のプレイを見るだけで十分に楽しいです。さらにソースコードが公開されているので、デスクトップ・スマートフォン・デジタルカメラ・オシロスコープなど、想像できるあらゆるモダンプラットフォームで遊べます。
このような理由から、さまざまな経緯を経て私は Fedora Linux で複数の DOOM 関連パッケージをメンテナンスしています。
Fedora の各リリース前にはすべてのパッケージを一斉に再ビルドする「Mass Rebuild」が行われます。これによって得られるメリットは以下の通りです。
- ABI 互換性の保証
- 静的リンク先ライブラリの更新
- コンパイラ最適化やコードハードニングオプションの活用
Fedora Linux 42 は 4 月中旬にリリース予定で、Mass Rebuild の時期も到来しました。いつものように、すべてのパッケージがビルドできたわけではなく、そのうち
chocolate-doom が失敗しました。
二度と「false」を使ってはいけない
まず何が起きたのかを知るためにビルドログを確認したところ、次のようなコンパイルエラーが出ていました。
gcc -DHAVE_CONFIG_H ... -c -o deh_bexstr.o deh_bexstr.c In file included from ../../src/sha1.h:21, from ../../src/deh_defs.h:21, from deh_bexstr.c:22: ../../src/doomtype.h:113:5: error: cannot use keyword ‘false’ as enumeration constant 113 | false, | ^~~~~ ../../src/doomtype.h:113:5: note: ‘false’ is a keyword with ‘-std=c23’ onwards
つまり、
false がキーワードになったために列挙子として使えないというエラーです。chocolate-doom のコードは次のように独自のブール型を宣言しています。
#if defined(__cplusplus) || defined(__bool_true_false_are_defined) typedef bool boolean; #else typedef enum { false, true } boolean; #endif
C++ モードなら標準
bool を使い、C モードでは自前の列挙型を使用します。旧 C 標準(C89)にはブール型が存在せず、C99 で
_Bool が導入されました。<stdbool.h> をインクルードすると bool, true, false のマクロが定義されます。C23 になると _Bool は bool に改名され、bool, true, false はすべてキーワードになります。
したがって、今回のエラーは「自前型とキーワードが衝突している」ことに他なりません。数か月前までは問題なくビルドできたのに、今なら失敗するとは何故でしょう? これはコンパイラが C23 を採用したためです。
一般的な変更点
Mass Rebuild の目的は「ディストリビューション内のコードを最新コンパイラでビルドできる状態に保つ」ことです。GCC のバージョン履歴を見ると、過去10年ほど毎年4〜5月ごとにメジャーバージョンが登場しており、Fedora のリリーススケジュールと合致しています。
今回も同様で、Fedora Rawhide(開発版)に GCC 15.0.1 が数時間前に導入されました。このバージョンの変更点の一つが「デフォルト C 標準を
-std=gnu17 から -std=gnu23 に変更した」というものです。ビルドログを見直すと、標準を明示的に設定するオプション -std= が存在しないことが分かります。
今後の対策
考えた結果、次の三つの解決策が浮上しました。
- C 標準を C17 以前に明示的に固定
- コードは自前ブール型でビルドされる。簡単だが将来を見越すとやり直しになる恐れ。
を変更して、C23 時には組み込み#ifdef
型を使用bool- より自然なコードになり得る。実装も容易。
- 列挙子名を
,False
に変えて全ての使用箇所を修正True- 安全だが面倒。
検討した結果、2番目を採用することにしました。
--- a/src/doomtype.h +++ b/src/doomtype.h @@ -#if defined(__cplusplus) || defined(__bool_true_false_are_defined) +#if defined(__cplusplus) || defined(__bool_true_false_are_defined) || (__STDC_VERSION__ >= 202311L) // Use builtin bool type with C++. +// Use builtin bool type with C++ and C23. typedef bool boolean;
このパッチは簡単に適用でき、Fedora パッケージはビルド成功しました。速やかに upstream にプルリクエストを送った次第です。
エンジンが崩壊する
提案を受けてメンテナの議論が始まりました。結局、プロジェクトを「C99 で書かれたもの」と宣言しようという方向に決まり、別のプルリクエストが作成されました。
--- a/src/doomtype.h +++ b/src/doomtype.h @@ #include <inttypes.h> +#include <stdbool.h> #if defined(__cplusplus) || defined(__bool_true_false_are_defined) -// Use builtin bool type with C++. - -typedef bool boolean; +typedef int boolean; #else
<stdbool.h> を追加するのは妥当です(C99 では必ず存在します)。しかし boolean の型定義を int に変えたことが不思議でした。これは「C99 に切り替えても実際には整数としてブール値を扱う」という意味になります。
その後の議論で次のように指摘されました。
明示的に C 標準を設定することは問題ないが、インクルードや型定義は変更しない方がよい。
を残すと Chocolate Doom は起動できない。typedef bool boolean
失敗例: R_InitSprites: Sprite TROO frame I has rotations and a rot=0 lump.
つまり
を使うとエンジンが起動時にエラーで終了してしまう。_Bool
実際に何が起きているのか
詳細なコードは省略し、簡潔にまとめます。
内でR_InstallSpriteLump()
は関数引数(変更されない)frame
はグローバル配列sprtemp
を保持spriteframe_t
フィールドはブール型.rotate
自前列挙型を使うと、条件判定が正しく機能しエラーにならずに済みます。
しかし
_Bool を使用すると比較が真となり、起動時にエラーで終了します。
GDB でのデバッグ
作業中は次のような挙動を確認しました。
(gdb) print sprtemp[frame] $1 = {rotate = (true | unknown: 0xfffffffe), ...}
sprtemp[frame].rotate は memset(..., -1, ...) により -1(すなわち 0xffffffff)で初期化されます。列挙型の場合、比較 == false は false となります。
しかし
_Bool を使うとサイズが 4 バイトから 1 バイトに縮小します。同じ初期化を行っても
(gdb) print sprtemp[frame] $1 = {rotate = 255, ...}
== false は 255 == 0 と評価され true になり、エラーが発生します。
最小再現例
#include <stdio.h> #include <string.h> #ifdef DUPA #include <stdbool.h> typedef bool boolean; #else typedef enum { false, true } boolean; #endif boolean some_var[30]; int main(void) { memset(some_var, -1, sizeof(some_var)); some_var[0] = false; some_var[1] = 500; for(int i = 0; i <= 2; ++i) { if(some_var[i] == false) printf("some_var[%d] is false\n", i); if(some_var[i] == true) printf("some_var[%d] is true\n", i); printf("value of some_var[%d] is %d\n", i, some_var[i]); } return 0; }
-DDUPA を付けてコンパイルすると _Bool が使われ、 255 の値が「真」とも「偽」とも評価される現象を確認できます。
アセンブリでの挙動
Godbolt でビルドしたアセンブリを見ると、
_Bool 用のコードは最低位ビットを反転させてテストするように生成されています。つまり比較が != 0 として扱われるため、値 255 が両方の条件を満たすわけです。
未定義動作(UB)
UBSan を使って検証すると、以下のエラーが報告されます。
gcc -DDUPA -fsanitize=undefined -o booltest ./booltest.c ./booltest ... runtime error: load of value 255, which is not a valid value for type '_Bool'
C99(6.2.6節)では、あるオブジェクト表現がその型の値を必ずしも表さない場合があります。文字型でない lvalue 式でそのような表現を読み取ると動作は 未定義 になります。今回のケースはまさにそれです。
参考文献
- Can It Run Doom?(リンク先)
- Stack Overflow: Difference between JE/JNE and JZ/JNZ
- Stack Overflow: How do AX, AH, AL map onto EAX?
- Wikibooks: x86 Assembly – Control flow
- Clang documentation: UndefinedBehaviorSanitizer
- N1256 — C99 標準の最終ドラフト