はじめに:NASAの10ルールとは?
NASAは、宇宙探査機など非常に高価でミッションクリティカルな組み込みシステムをターゲットとして、C言語によるプログラム開発のための10のルールを提示しています。
これらのルールは、いかにバグを減らし、安全性を高めるかという観点から極めて厳しい制約を設けています。しかし同時に、「なぜAdaのSPARKサブセットを使わないのか?」という疑問や、「もっと良いプログラミング言語だったら?」といった別の視点もありえます。
なお、ここで書かれている批評は「プログラミング言語処理系(コンパイラやインタプリタ、エディタなど)やアプリケーションソフトウェアの開発者側」の観点からのものです。批評の主旨は「それぞれの文脈を考慮せずにルールを盲目的に採用するのは危険」という点にあります。
1. 制御フロー構造を単純化する(goto、setjmp/longjmp、再帰を禁止)
原文要約:
- goto文やsetjmp(), longjmp()、さらに直接・間接的な再帰を使わないようにする
- Cではsetjmp()とlongjmp()は例外処理として使われることがあるが、それも禁止となる
批評:
- 再帰やジャンプを禁止し、ループにも明示的な上限を設けることで「プログラムが必ず終了する」ことを保証しやすくなります。しかし、「必ず終了する」といっても途方もなく長い実行時間になれば、実質的に「終わらない」のと変わりません。
- 自然に再帰で書ける問題を無理にループとスタック擬似管理で書き換えると、可読性が下がったり、バグが増えたりします。
コード例:膨大なイテレーション
int const N = 1000000000;
for (x0 = 0; x0 != N; x0++)
for (x1 = 0; x1 != N; x1++)
...
for (x9 = 0; x9 != N; x9++)
-- do something --;
上記は固定された上限付きループですが、N=10^9で10重ループにすると、最大イテレーション回数は10^90となり、1反復1ナノ秒でも10^81秒、約7.9×10^72年かかります。
2. すべてのループには固定の上限を設ける
原文要約:
- ループの反復回数に明確な固定上限があり、静的解析でその上限を証明できる必要がある
- 上限が証明できなければルール違反となる
批評:
- これは組込み向けの古典的な考え方です。前述したように「上限がある」だけでは実用的な保証にはなりません。
- 現実的には「上限が適切に小さいこと」が重要で、それを破った場合に「実行時エラーとして扱う」などの設計が必要です。
- 実は再帰にも再帰の深さの上限を与えれば、同様に「ループの固定上限」と同等の安全性を得られます。
3. 初期化以降、動的メモリ割り当ては使わない
原文要約:
- プログラム開始(初期化)後はmalloc()やfree()を使用しない
- 組込みではメモリが限られるので枯渇を回避するため
- パフォーマンス上も動的割り当ての挙動は予測しづらいため、としている
批評:
- これも古典的な方針で、組込み向けにメモリ割り当て機能をそもそも持たない言語も存在します。
- ただし、「まったく動的割り当てをしない」ではなく、「動的割り当ての仕組みを使っても常に必要なメモリ量を証明できる」アロケータを運用し、メモリ枯渇が起こらないようにすればよいという考え方もあります。
- malloc()/free()を自前の配列・リスト管理でエミュレートすることは形式的にはルールを守っていても、実質的にはさらに柔軟性を損ない、かえって危険な場合もあります。
4. 1つの関数は1ページ以内(約60行)に収める
原文要約:
- 1関数は紙1枚に印刷できる程度(60行前後)にすること
- 1行1宣言・1行1文の標準的なフォーマットで想定
批評:
- 現代では紙ではなくエディタや画面でコードを読む場合が多いので、「紙1枚」という基準はやや時代遅れ感があります。
- 本質的には「プログラマーが一度に理解すべきコード量を小さく保つ」のが重要でしょう。
- ネストした関数や折りたたみ機能など、言語や環境によってコードをスッキリ見せる技法があるため、「純粋な行数」だけでは測れない面も多々あります。
5. 関数あたり最低2つのアサーションを入れる(アサーション密度)
原文要約:
- コード中のアサーションを増やし、「関数あたり2個以上」の密度を保つ
- アサーションはドキュメントとしてもデバッグ用にも強力である
批評:
- アサーションはたしかに有用ですが、「関数が最大60行」と決められている中で「最低2つ」と言われても、どう配置すべきかはケースバイケースです。
- 外部からの入力や引数に対する不変条件チェックは重要なので、型システムで保証できない部分をアサートするのは有益でしょう。
例:アサーションの書き方
// 可読性が低い例
if (!c_assert(p >= 0) == true) {
return ERROR;
}
// より自然な例
if (!c_assert(p >= 0)) {
return ERROR;
}
// マクロを使用した読みやすい例
check(ERROR, p >= 0);
6. 変数は可能な限りスコープを絞って宣言する
原文要約:
- 変数(データオブジェクト)は使うべき範囲の中で最小スコープに収めて宣言せよ
批評:
- これは非常に良い方針です。変数だけでなく、関数や型宣言に対しても同様に適用すると良いでしょう。
7. non-void関数の戻り値は常にチェックし、引数の妥当性は関数内部でチェックする
原文要約:
- 戻り値がエラーコードで返ってくる場合など、呼び出し側は必ずチェックする
- 関数内部でも引数の有効性をチェックする
批評:
- C言語のように「エラーが戻り値で示される」場合は重要な考え方です。
- ただし、本当にすべての引数や要件をチェックするとなるとオーバーヘッドがかかりすぎることもあります。
- ルール違反しがちなC標準ライブラリなどは一切をチェックしない設計になっていたりするので、実運用では方針と現実の狭間で苦労する部分です。
8. プリプロセッサの使用はヘッダファイル取り込みと単純なマクロに限定する
原文要約:
- トークンの貼り付け(##)や可変引数(...)、再帰的マクロ呼び出しは使わない
- 条件付きコンパイル(#if)なども極力避ける
批評:
- 可変マクロ(...)を使ったデバッグ用ログ出力などは便利なので、一概に禁止するのはもったいない場合もあります。
- マクロによるコード折りたたみなど、可読性向上に貢献するケースもあり、それらを一律に禁止すると却ってコードが煩雑になりうる面があります。
- 条件付きコンパイルを完全に禁止しても、実行時にif文などで分岐するコードをどのみち全部テストしなければならないのは同じです。
9. ポインタ使用は制限する(多重間接禁止、関数ポインタ禁止など)
原文要約:
- 間接参照は1段階まで
- マクロやtypedefの中に隠れたポインタ参照も不可
- 関数ポインタを使うことは禁止
批評:
関数ポインタの禁止について:
- 数値解析などで「被積分関数を引数に渡す」ようなシグネチャを設計したい場合、Cでは自然と関数ポインタが登場します。
- 関数ポインタを禁止すると、代わりにenumとswitchで関数を呼び分けるようなコードが必要になり、かえってバグが入りやすくなります。
多重間接への批判:
- 多重間接禁止が「抽象データ型(ADT)」の実装にも影響します。
- ADTによる情報隠蔽はソフトウェア工学の重要な原則の一つです。これを阻害してしまうと、保守性や堅牢性に影響が出るでしょう。
10. コンパイラ警告は最も厳密な設定で"警告ゼロ"にし、少なくとも1つ以上の静的解析ツールでデイリーチェックする
原文要約:
- 開発開始時から常に厳密なコンパイラ警告オプションを有効にする
- 毎日ビルド時に最新の静的解析ツールでもチェックし、警告を0に保つ
批評:
- これは非常に良いアドバイスです。
- ただし、ここで挙げられているポインタ禁止や再帰禁止といった厳しいルールは、もともと「静的解析ツールの限界に合わせてコードを制限する」という意図もあります。
- もしもっと解析しやすい言語(あるいは厳密なサブセット)を採用できるのであれば、ここまで厳しく制限しなくても済む可能性は十分あります。
まとめと感想:NASAの10のルールから得られる教訓
NASAが提示しているこれらのルールは、「極端に高い安全性・信頼性を追求する場面」では一理あるものです。特に宇宙関連のミッションでは、一度のエラーが数十億円、数千億円もの損失につながりかねないため、「プログラムが絶対に終わる」「メモリが絶対に足りる」といった形式的保証が非常に大切です。
しかし、だからといって「どんなソフトウェアでも同じルールを適用するべき」とはなりません。大切なのは「自分たちの文脈や要求事項は何か」を踏まえて、ルールを取捨選択あるいは修正することです。どんなに権威ある(あるいは経験豊富な)人の意見やガイドラインでも、盲目的に従うのではなく、自分たちの状況に合うかどうかをよく考えることが重要ですね。