longjmp() 関数と例外処理
C 言語の野放図なパワーを見せつけてくれるのが longjmp()
関数だ。とりあえず、サンプルコードを見てみよう:
#include <setjmp.h> #include <stdio.h> void f(jmp_buf jmpBuf) { puts("f() called."); longjmp(jmpBuf, 1); } int main() { jmp_buf jmpBuf; setjmp(jmpBuf); puts("calling f()."); f(jmpBuf); return 0; }
これを実行すると、「calling f().
」と「f() called.
」が標準出力に無限に書き出され続ける。なぜなら、longjmp()
の呼び出しは setjmp()
の呼び出しからリターンするからだ。素晴らしい。はっきり言って、眩暈がする。
“リターンしない関数 (メソッド)”というのは珍しくない。Java にも、java.lang.System#exit(int)
メソッドというものがある。だが、“他の場所からリターンしてくる”なんていう無茶をするのは、この longjmp()
と return-into-libc 攻撃ぐらいのものだろう((longjmp()
関数がどのように実装されているかを考えることによって return-into-libc 攻撃がより理解しやすくなり、スタック上のバッファをオーバフローさせることの危険性も理解できるのではないかと思う。))。
さて、こんな代物に使い道はあるのか。実は、この longjmp()
を使うと、例外処理もどきを作れる:
#include <setjmp.h> #include <stdio.h> #include <stdlib.h> void maybe_fail(jmp_buf onError, const char* message) { if (!message) { longjmp(onError, 1); /* ここの「1」という値に意味はなく、どんな値でも構わない */ } puts(message); } int main() { jmp_buf jmpBuf; /* setjmp() の呼び出しは、longjmp() の呼び出しからのリターンである場合は * 0 以外の値を返し、そうでない場合は 0 を返す。 */ if (!setjmp(jmpBuf)) { maybe_fail(jmpBuf, "Hello."); maybe_fail(jmpBuf, NULL); } else { fputs("-Error-\n", stderr); return EXIT_FAILURE; } return 0; }
“失敗するかもしれない”関数 maybe_fail()
は、失敗した場合の飛び先として jmp_buf
型の引数を要求する。呼び出し側は、そのための jmp_buf
型変数を setjmp()
で用意しなければならない。だが、これにより、全てのエラー処理を else
節の1箇所に集約できている。
上のサンプルコードで、「if (!setjmp(jmpBuf))
」が「try
」に、「else
」が「catch
」に見えてこないだろうか? メッセージ文字列を持たせたり例外の種類を階層化したりといったことはできていないが、それとて、適当な構造体を用意してやれば実現できる。そう、C 言語でも例外処理 (のようなもの) は実現できるのだ。
ただし、実際にやって嬉しいものなのかは、C よりも C++ の方がいいと信じている僕には定かではない。