[C言語] setjmp() と longjmp() の使いかた

C言語の標準ライブラリ関数 setjmp() と longjmp() を呼び出すことで多段の関数呼出階層を飛び越えるジャンプ(いわゆるGOTO処理)を実現できます。しかしながら、現代的なプログラミングでは GOTO文 が忌避されるように、setjmp() と longjmp() を使ったジャンプは推奨されません。やむを得ず setjmp() と longjmp() で実装された既存のソースコードを理解するための助けとなることを目論んだ解説です。

多段の関数呼出階層
関数の中から下位の関数を呼び出して、その下位の関数の中から下位の下位の関数を呼び出して、その下位の下位の関数の中から下位の下位の下位の関数を呼び出す、といった構造

1. 単純なロングジャンプの例

1.1. サンプル・ソースコード

1.2. サンプル・ソースコードの実行結果

  1. 関数 main() の中から top_function() を top_function() の中から middle_function() を middle_function() の中から bottom_function() を順に呼び出しています。
  2. 関数 main() の中で top_function() を呼び出していますが、次行の printf() を実行していません。
  3. 関数 top_function() の中で middle_function() を呼び出していますが、次行の printf() を実行していません。
  4. 関数 middle_function() の中で button_function() を呼び出していますが、次行の printf() を実行していません。
  5. 関数 buttom_function() の中で標準ライブラリ関数 longjmp() を呼び出していますが、次行の printf() を実行していません。
  6. 関数 buttom_function() の中で標準ライブラリ関数 longjmp() を呼び出した直後に main() 中のelse句を実行しています。

1.3. シーケンス図

1.3.1. return文で関数呼び出しを1階層ずつ呼出元へ戻ったばあい

return文のシーケンス

1.3.2. longjmp()で関数呼び出しの先端から一足飛びに最上位の呼出元へ戻ったばあい

longjmpのシーケンス

1.4. 解説

関数 main() の中で setjmp() を呼び出したタイミングで、longjmp() からジャンプ(復帰)してきたときに復元するスタック情報を静的な変数 jmp_buf jump_buffer に保存しています。もし、longjmp() で復帰してくる前に jump_buffer の内容を壊してしまうと、ジャンプで戻ってきたのは良いものの関数 main() は続きの処理を正しく実行できません。もし longjmp() で戻ってこなければ jump_buffer に保存した情報は不要になります。

関数 setjmp() を最初に呼び出したときは 必ず 0 (ゼロ) を返します。次に longjmp() からジャンプ(復帰)してきたときは必ず非ゼロを返すため、else句に分岐します。

関数 top_function() だけに着目すると middle_function() からreturn文で戻ってきて次行の printf() を実行することを期待します。しかし longjmp() で一足飛びに関数 main() に戻って(ジャンプして)しまうためソースコードから動作を追跡することが難しくなります。

同様に middle_function() だけに着目すると buttom_function() からreturn文で戻ってきて次行の printf() を実行することを期待します。しかし longjmp() で一足飛びに関数 main() に戻って(ジャンプして)しまうためソースコードから動作を追跡することが難しくなります。

上記の例では関数 bottom_function() の中で必ず longjmp() を呼び出しているため、常に button_function() から top_function() までのreturn文が省略(スキップ)されています。しかし longjmp() を呼び出す条件を分岐すれば、関数から抜け出る手順(戻っていく先)がマチマチにあります。もしくはマチマチにすることを目的にして longjmp() を利用します。

2. 繰り返し処理の中でロングジャンプをつかった例

2.1. サンプル・ソースコード

2.2. サンプル・ソースコードの実行結果

  1. 関数 start_engine() の中で乱数を生成し、longjmp()を実行したりしなかたり分岐しています。
  2. 関数 start_engine() の中でロングジャンプを実行しないときは、関数 countdown() の中の処理(含むforループ)を全て実行します。
  3. 関数 start_engine() の中でロングジャンプを実行したときは、関数 countdown() の中の続きの処理が丸っとスキップして、 main() に戻ります。

2.3. 解説

いわゆるエラーが発生したときに、以降の処理を丸っとスキップする流れが簡易に実現できます。この例では関数呼び出しが2階層と浅く、longjmp() を呼び出す箇所が一箇所であるため、ソースコードから全体の処理の流れを追跡するいことは比較的容易です。しかし、関数呼出しが4階層、5階層と深くなり、longjmp() を使ってジャンプする箇所が複数存在すると、メンテナンスやデバッグが難しいプログラムになります。

3. ロングジャンプの弊害

  1. プログラムに必要な後始末(たとえばOpen処理に対するClose処理)が漏れる(意図せずにスキップする)ミスを誘発します。
  2. ソースコードの『ここ』を通過するはず、とprintfデバッグを埋め込んだり、デバッガでブレイクポイントを仕掛けても、するっと通り抜けてしまいます。

4. 参考リンク