F-35で使われたC++標準 — なぜ戦闘機はC++機能の90%を禁止するのか
(youtube.com)- マッハ1の速度域では、1行のバグが致命的な結果につながり得る戦闘機・ロケットのソフトウェアで、なぜC++機能の大半を削ぎ落として予測可能なコードだけを残すのかを解説する動画
- F-4の機械式爆撃コンピュータ、非公開だったF-14用マイクロプロセッサ、Jovial・CMS-2・Adaへと続く軍用言語の対立とコード爆発が、単一の安全言語と厳格な標準を求めるに至った歴史を整理
- F-35開発でLockheedがAdaではなくC++の採用を認めさせるために作ったJSF C++標準が、例外・再帰・動的メモリ割り当てを禁止し、戻り値コード・反復ループ・事前メモリ確保などで置き換える方法を実際のコードで実演
- 初期のF-35ミッションコンピュータがPowerPC系アーキテクチャを使っていた点とあわせ、X-Plane 12と自作MFDを接続して、JSFルール違反コードと準拠コードが実際の飛行中にどう違うかを比較
- JSF標準がその後NASA F-Prime・MISRA・AutoSARのような安全性標準へとつながり、現在ではその遺産の上に**C++ Core GuidelinesとモダンC++**を使う方向がより適切だと結論づける
発表者紹介
- 航空宇宙システム向けのC++コードを書き、空軍向けデモを行っていた経験のある開発者
- 実際に安全性要求の高いシステムでC++を使った経験をもとに説明
- X-Plane 12、Web API、Python UI、C++バックエンドで構成した**自作MFD(多機能ディスプレイ)**をデモ環境として使用
飛行ソフトウェアと失敗が許されない環境
- 一般的なアプリケーションではクラッシュしても再起動で済むが、マッハ1の戦闘機・ロケットでは1度の失敗がそのまま災害になる環境
- **「マッハ1ではガベージコレクタを待つ時間はない」**という一文でリアルタイム性の要求を強調
- 1行の誤ったコードが致命的結果につながる状況では、言語機能の選択そのものが安全装置であるべき
- 1996年のAriane 5爆発事故を代表例として提示
- 水平バイアスの64ビット浮動小数点値を16ビット整数に変換する過程で例外が発生し、その例外を処理できなかった
- 言語は仕様どおりにエラーを発生させたが、システムがその例外を処理できず、5億ドル規模のロケットが即座に破壊される結果につながった
- この事例を基準点として、F-35設計チームは同じ失敗を繰り返さないために、言語機能そのものを切り落とすアプローチを選んだ
軍用ソフトウェアの歴史と言語戦争
- 初期戦闘機のF-4 Phantom時代には、事実上ソフトウェアが存在しなかった
- 爆撃コンピュータはギアとカムで構成された精密機械装置に近く、「コード」は金属カムの形状そのものだった
- 極秘計画だった海軍制空戦闘機**VFX(F-14 Tomcat)**で状況が変わる
- 教科書で「最初のマイクロプロセッサ」として知られるIntel 4004より前に、Garrett AiResearchがF-14用マイクロプロセッサを設計していたことが後に公開された
- F-14の可変翼(swing wing)を最適制御するため高性能マイクロプロセッサが必要で、そこに約2500行規模のマイクロコードが焼かれ、多項式計算を実行していた
- その後、各軍が異なる言語を採用し、言語の乱立が始まる
- 空軍はALGOL系の**Jovial(Jules Own Version of the International Algorithmic Language)**を使用
- 海軍は空軍の言語を使わないとして、F-18にCMS-2を採用
- 異なる言語・ハードウェアアーキテクチャを使うことで、コード再利用と検証はほぼ不可能な状態になった
- 同時に、航空機1機あたりのソフトウェア規模は指数関数的に増加した
- F-16Aは約12.5万行、B-1は約100万行、現代のF-35は約900万行へ増加したという数値例を提示
- 国防総省の調査では、450を超えるプログラミング言語が使われており、まともな標準を持つ言語はほとんどなかったと報告された
Ada強制とその限界
- こうした乱立を解決するため、国防総省は単一の高級言語Adaを作り、強力な使用義務を課した
- 新規プロジェクトでAdaを使わないなら、「なぜAdaでは不可能か」を証明しなければならず、そうでなければ契約を獲得できない仕組みだった
- Adaは安全性・信頼性が重要な分野には非常に適した言語として紹介される
- 航空宇宙のような安全クリティカルシステムで、メモリ安全性や型安全性を確保する設計が強調された
- しかし90年代に入ると、現実との乖離が見え始める
- 一方ではインターネット、Windows 95、C++ベースの商用ゲームが主流を形成
- 学生や開発者は高価なAdaコンパイラではなく、**無料のGCCとC++**へ自然に流れた
- Adaコンパイラは数千ドルに達し個人には触れにくく、その結果Ada専門人材のプールも縮小していった
F-35とJSF C++標準の誕生
- **F-35(統合打撃戦闘機、Joint Strike Fighter)**は、最初からソフトウェア比重が非常に大きい機体として設計された
- センサーフュージョン(sensor fusion)のような複雑な計算が運用の中核となった
- Lockheed Martinは、こうした要求のためにC++使用の許可を国防総省に提案した
- 既存のAda強制方針を事実上「破ってほしい」という要請であり、それを説得するにはC++の危険性を制御する方法が必要だった
- この過程で、C++の生みの親Bjarne Stroustrupも助言者として参加し、JSF C++ルールの設計に関わったと述べる
- 自身がJSFルール策定を直接手伝ったと明かし、そのためこの標準に対して「バイアスがあるかもしれない」と自ら言及している
- 中核アイデアは**「remove before flight」**タグと同じ発想
- 飛行前に外すべきタグのように、C++の危険な機能を言語レベルで取り除き、予測可能なサブセットだけを使う方式
- これによりAda並みの安全性を確保しつつ、開発者はC++の表現力をある程度活用できるようになった
実機ハードウェアとGameCubeのたとえ
- 初期のF-35ブロックは、Motorola G4 PowerPCプロセッサベースのミッションコンピュータを採用していた
- この情報は2003年のAviation Today記事などで公開されている内容
- GameCubeもPowerPC系プロセッサを使っているため、命令セット水準で近い世代のハードウェアという点が興味深い
- 世代をより厳密に合わせるなら別のコンソールの方が近いが、原理を理解するにはGameCubeのたとえで十分
JSF C++デモ環境: X-Plane 12 + MFD
- X-Plane 12をベースに、F-35Bアドオン(AOA Simulations)を使って飛行シミュレータ環境を構築
- X-Planeの新しいWeb APIでリアルタイム飛行データを購読し、それをMFDに表示する構成
- フロントエンドはPythonで構成され、バックエンドはC++プラグインとして書かれ、JSFルールに従うコードと違反するコードを比較実演
- 高度、速度、風、飛行エンベロープ、ナビゲーションデータなど各種情報をC++の計算結果として表示
- 飛行中に意図的に例外を投げる非標準C++コードを実行してMFDが落ち、飛行に問題が起きる状況を見せた後、JSFルールを適用してこれを解決する過程を段階的に示す
JSFの3つの中核制約: 例外、再帰、動的メモリ
- JSF C++標準で強調される3つの中核制約は、例外(Exception)、再帰(Recursion)、動的メモリ割り当て
- これに加えて、関数の**Cyclomatic Complexity(循環的複雑度)**の上限も明記される
1) 例外禁止 – AV Rule 208
- JSF AV Rule 208: 「例外は使用しない(exceptions shall not be used)」
try、catch、throwなど例外関連キーワードを全面禁止
- 中心的な理由は、制御フローの非決定性にある
- Ariane 5のように例外発生時に処理漏れやタイミングずれがあると、システム全体が予測不能に崩壊し得る
- BjarneはモダンC++の例外処理には好意的だが、JSFが設計された当時はツール成熟度とリアルタイム性保証の問題から例外を支援できなかったと説明する
「JSF++はハードリアルタイム・安全クリティカルアプリケーション(飛行制御)向けであり、計算に時間がかかりすぎれば人が死ぬ可能性がある。例外では応答時間を保証できない」
- JSFでは例外の代わりに**戻り値コード(return code)**を使う
- 密度高度(density altitude)計算の例では、エラー状況ごとに異なる戻り値コードを設定し、呼び出し側がそれを解釈して処理するよう構成
- 計算失敗やタイムアウトが発生しても、プログラム全体が落ちず、呼び出し側で「エラー状態」を安全に表現できる
2) 再帰禁止 – AV Rule 119
- AV Rule 119は、関数が直接・間接に自分自身を呼び出すこと、つまり再帰を禁止する
- 再帰呼び出しのたびに新しいスタックフレームが積まれ、最大深度の上限が分かりにくく、スタックオーバーフローの危険が高まるため
- 飛行計画中の代替空港計算に使う**二項係数(binomial coefficient)**を例として挙げる
- 非標準版では
C(n, k) = C(n-1, k-1) + C(n-1, k)という形の再帰実装を使う - これは呼び出し深度が入力に応じて変化し、メモリ上限を予測しにくい問題がある
- 非標準版では
- JSF準拠版では、同じ計算をループベースの反復的(iterative)実装に変更する
- コードは長くなり美しさは減るが、自分自身を呼ばないため、再帰なしで同じ結果を返せる
- こうすることで、関数呼び出し深度とメモリ使用上限を静的に推論しやすくなる
3) 動的メモリ割り当て禁止 – AV Rule 206
- AV Rule 206: 初期化後はメモリの割り当て・解放を行わない
- 実行中の
new、delete、スマートポインタ経由の内部newなど、ヒープ割り当てを禁止
- 実行中の
- 理由は大きく2つある
- ヒープ割り当てには、どれだけ時間がかかるか分からない時間的非決定性がある
- ヒープが断片化(fragmentation)すると、総容量が残っていても大きな連続ブロックを見つけられず、割り当て失敗が起こり得る
- 例として、突風(gust)計算のためにIAS(指示対気速度)履歴バッファを
std::unique_ptr+ 動的配列で作る非標準コードを示す- 実行中に新しい配列を割り当てるため、JSFルール違反となる
- JSF準拠版では、最大サイズを定数で定義した固定長配列を使用する
MAX_IAS_HISTORYのような定数を定義し、初期化時に1度だけメモリを確保した後はインデックスだけを回して運用する- これにより、実行中には追加割り当てが一切発生せず、時間・メモリ両面で予測可能性を確保できる
4) Cyclomatic Complexity上限 – AV Rule 3
- AV Rule 3は、関数の**Cyclomatic Complexity(循環的複雑度)**が20を超えてはならないと定める
- 関数宣言自体で1点、
if・while・for・switch・論理AND/ORなどがすべて複雑度に1ずつ加算される
- 関数宣言自体で1点、
- 二項係数の反復実装を例に、各
if・論理演算・forループが複雑度ポイントをどう増やすかを示す- 複雑度が高くなるほど、テスト・検証・解析が難しくなるため、一定上限以下に保つことが標準の目標となる
JSFの遺産: NASA F-Prime、MISRA、AutoSAR
- JSF C++標準のアイデアは、その後ほかの安全クリティカル分野へ広がっていく
- NASAの飛行ソフトウェアフレームワークF-Primeは2017年に公開され、動的メモリ割り当て禁止、例外禁止、再帰禁止といったルールを共有している
- 自動車業界でも同様の流れが続く
- MISRA C++、**AutoSAR(Automotive Open System Architecture)**のような標準が登場し、自動車ソフトウェアの安全ルールを提示
- AutoSAR C++14ガイドはJSFを明示的に参照したとされ、JSFの影響が自動車ソフトウェアにも及んでいることを示す
- 現代の自動車は事実上**「車輪の付いたコンピュータ」**に近く、このような言語サブセットやコーディング規則が安全を支える中核基盤となっている
結論: 今日C++を使うなら何に従うべきか
- JSF C++標準は、当時としては複雑な言語を予測可能なサブセットへ縮小し、戦闘機の飛行制御レベルの安全性を確保したエンジニアリング成果として示される
- 同時にBjarne Stroustrupは、今日の開発者にはC++ Core GuidelinesとモダンC++に従うことを勧めている
- C++言語とツールチェーンがこの数十年で進化し、例外やスマートポインタのような機能も安全に使える環境が多く整ってきたため
- それでもJSFは、依然として言語を追加ではなく「削ることで」制御する考え方の重要な事例として残る
- 何を入れるかより、何をremove before flightリストに載せるかが、安全クリティカルシステム設計の核心だというメッセージで締めくくられる
8件のコメント
軍事目的であるため、低いハードウェアスペックと高度な機能の間でバランスを取った結果が C++ になったのではないかと思います。
学生や開発者が学ばなかったのは、国防省向けの言語で需要が少ないからであって、ツールが高価だからではないでしょう。
全体として、予測可能なコードを書くための手法についての内容ですね。
C++が例として挙げられているだけで、GCのように他の言語ではさらに問題になりうる部分も同様なのに、C++の限界を論じているかのように受け取られかねない点は残念です。
C++の動的割り当てや例外処理の手法を使わないのであれば、Cを使って上記の原則で書くほうが、はるかに簡単で速いのではないか、という疑問が残ります。
そうですね、なぜCではなくC++なのか分かりませんね
モダンなC++は本当に安定していますが、必ずしもC++である必要がないなら、より安定した別の言語を検討してみるのもよいのではないかと思います。
RUSTの文法をSTRICTに使おう!
Hacker Newsの意見
142ページあり、実際の航空機ソフトウェア開発でどのような制約があるのかがわかる
ただし「shall」ルールの例外が気になる。こうした大規模プロジェクトでは、例外ケースが標準の実効性を示す
2005年当時なら問題なかっただろうが、今どきのAIベースのドローンのような環境ではどう適用されるのか気になる
とくに
stdio.h禁止のような部分は違和感があるが、読んでいくと「なるほど、その通りだ」と思えてくるa = a;のような構文を思い出した未使用パラメータを削除しないための小技だが、こうしたルールが良いコード品質を保証するのかは疑問だ
JSFガイドラインは2005年の文書なので時代的な限界がある
Bjarne Stroustrupが最近「C++プロファイル」という概念を提示したのも興味深い
結局のところ、こうしたスタイルガイドは美的嗜好の問題なのかもしれない
(void)a;で処理するのが標準的なやり方だ本当に安全を保証するのは設計品質とフェイルセーフ構造だ
_ = a;で明示的に処理する関連イシューもある
むしろレビュー時に参照するための共通基準を提供する役割だ
核心は**ミッション保証(mission assurance)**だ
スタックやヒープを使うと変数のメモリアドレスが変わるが、特定のセルが故障すると問題が起きる
すべての変数が固定アドレスを持っていれば、故障したセルだけをパッチで移動して任務を継続できる
ローカル変数、引数、戻りアドレスなどは結局スタックが必要になる
再帰も不可能になり、一時変数も制限される
だからこの主張は現実的ではない
スタックとヒープのアドレス範囲は固定できるが、それが説得力のある理由なのかは疑問だ
私も似たように「このケースは絶対に発生してはならない」といったログを残す
大規模システムでは、こうした例外状況のログ記録がデバッグに非常に有用だ
すべてのケースを明示的に処理しなければならないので、enum値が追加されるとコンパイラが警告してくれる
_STOPや_CRASHマクロを追加して、即座にデバッグ停止させることもある問題をすぐ修正させる効果がある
要するに「Ada開発者とツールチェーンが不足していた」という理由だった
しかし今では言語の多様性への開放性が高まっており、Adaもより受け入れられるかもしれない
NVidiaがSPARKを採用したのも、Adaの復活の兆しのように見える
DoDがAdaを強制していたなら、大学や企業も追随していただろう
結局言語は学べるのであり、Adaを使っていたならF-35の信頼性ももっと高かったはずだ
AdaCore, GHS, PTC ApexAda, DDC-I, Irvine, OC Systems, RR Software などだ
以前はAdaコンパイラが有料オプションで、C/C++は標準提供だったため、学校や企業はC++を選んでいた
AdaCoreはISO標準化とオープンソース普及に大きく貢献した
昔は過剰に批判されていたが、今ではむしろ魅力的に感じる
ファイル構成やビルドフラグが複雑で、RedMonk言語ランキングにもAdaは入っていなかった
一部の機能は興味深かったが、Rustのような現代的な言語体験ではなかった
DO-178Cのような認証体系が重要になる
むしろこれが高信頼性コードを書くための正しいやり方かもしれない
リバースエンジニアリング、難読化、コンパイラなど幅広いテーマを扱っている
こんなことまでわざわざ明記する必要があるのかと思った
LMが自作したのか、それとも商用製品なのか知りたい