ククログ

株式会社クリアコード > ククログ > ノータブルコード3 - 危険なバグを仕組みで予防する

ノータブルコード3 - 危険なバグを仕組みで予防する

第三回目のノータブルコードで取り上げるのは、分散バージョン管理システムGitのヘッダファイル「banned.h」です。

banned.h とは何か?

これは何かと言うと、その名前の通り「危ない関数の利用を禁止する」ためのヘッダです。実際にコードを見てみましょう。

#ifndef BANNED_H
#define BANNED_H

/*
 * This header lists functions that have been banned from our code base,
 * because they're too easy to misuse (and even if used correctly,
 * complicate audits). Including this header turns them into compile-time
 * errors.
 */

#define BANNED(func) sorry_##func##_is_a_banned_function

#undef strcpy
#define strcpy(x,y) BANNED(strcpy)
#undef strcat
#define strcat(x,y) BANNED(strcat)
...

https://git.kernel.org/pub/scm/git/git.git/tree/banned.h

ここで注目したいのは、冒頭に定義されている BANNED というマクロです。そもそも、この banned.h がどうやって関数の使用を禁止しているかというと、「禁止したい関数の呼び出しを、まったく別の関数の呼び出しに書き換える」という方法でこれを実現しています。この書き換えを担当しているのが BANNED マクロで、例えば最初の strcpy を例にとると、これを sorry_strcpy_is_a_banned_function に強制的に書き換えてしまいます。

禁止された関数を使うとすると何が起きるのか?

実際にこのヘッダをインクルードして、禁止された関数を使ってみましょう。今回はこんなコードを用意しました。

#include <string.h>
#include "banned.h"

int main(int argc, char *argv[])
{
    char buf[16];
    strcpy(buf, "aiueo");
    return 0;
}

このコードをコンパイルすると何が起きるかというと、未定義の識別子エラーが発生してコンパイラがコケます。コンパイルが失敗する原因は sorry_strcpy_is_a_banned_function() という関数が(当然のことながら)存在しないためです。実際のエラーメッセージを以下に示します。

$ cc test.c
In file included from test.c:2:
test.c: In function ‘main’:
banned.h:11:22: error: ‘sorry_strcpy_is_a_banned_function’ undeclared (first use in this function)

ここで非常に面白いのは、BANNED マクロが書き換えた後の関数名が、そのままエラーメッセージになっていることです ("Sorry strcpy is a banned function") 。つまり、ここで BANNED マクロは (1) コンパイル処理を失敗させ (2) さらに失敗した理由を開発者に伝える、という一人二役を担っているのです。

ビルドシステムに組み込まれた安全

ヒューマンエラーを低減する為の最も効果的な設計技法の1つは、エラーが物理的に不可能であるか、あるいはエラーが明白なように設計することである。例えば、連結部分のサイズが異なるように設計すれば弁を取り違えることはできなくなる。あるいは、非対称、すなわちオス/メスの連結にすることで1つの方法でしか組み立てられないようにすることは可能である。カラーコーディングで接続の誤りを明示することができる。同等の技法を、オペレータとコンピュータのインターフェイス設計に利用することができる。 ナンシー・G・レブソン 『セーフウェア - 安全・安心なシステムとソフトウェアを目指して』(2009年, 翔泳社)p447

「システムの安全性をどのように実現するか?」という問題は、システム工学の中心的な課題の一つですが、その一つの知見に「ヒューマンエラーの大部分は、実は設計の欠陥である」というものがあります。単純に「人間のミス」として片付けてしまわず、ヒューマンエラーを引き起こした設計を分析して、システム面から対策を講じない限り、次の事故を防ぐことは困難なのです。

今回紹介した BANNED マクロは、システムに組み込まれた安全の一例です。strcpy()strcat() が深刻な不具合を生みやすい機能であることについては、長い議論の積み重ねがあります。ヒューマンエラー防止の観点から見ると、この BANNED マクロは次の3つの特長を持っています。

  1. コーディング規約のような約束ではなく、マクロによる書き換えという仕組みにより、危険な実装を防いでいる。

  2. 単に警告メッセージを出すだけではなく、規約に反するコードはコンパイルが失敗するようにしている。

  3. すべてのコンパイラでも動作する汎用的な制限ロジックを、独自に実装している。

このうち(3)は、手法面で興味深い部分です。例えば、特定のプラットフォーム(例えばGCC)を前提にしてよいなら、#pragma GCC poisonといったコンパイラ独自の機能を使うことで、同じ目的を達成することができます。これにはコンパイラの支援を受けられる(より詳細なエラーメッセージが出せるなど)というメリットがありますが、その一方で他のプラットフォームではうまく機能しなくなるという別の問題を抱えることになります。

今回の BANNED マクロは、コンパイラの支援を受けずに、原因を明解に示すエラーメッセージを出力しているという非常に面白い試みです。

まとめ

皆さんも、コーディング規約を一歩進めて、仕組みによってコードの健全性を担保するようにしてみませんか? 皆さんが使っている、効果的な工夫やアイデアがあれば、ぜひとも教えてください。