[C言語] setjmp() と longjmp() の使いかた
C言語の標準ライブラリ関数 setjmp() と longjmp() を呼び出すことで多段の関数呼出階層を飛び越えるジャンプ(いわゆるGOTO処理)を実現できます。しかしながら、現代的なプログラミングでは GOTO文 が忌避されるように、setjmp() と longjmp() を使ったジャンプも推奨されません。やむを得ず setjmp() と longjmp() で実装された既存のソースコードを理解するための助けとなることを目論んだ解説です。
1. 単純なロングジャンプの例
1.2. サンプル・ソースコード
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
jmp_buf jump_buffer; /* longjmp() から復帰したときに復元するためのバッファ */
void top_function(void);
void middle_function(void);
void bottom_function(void);
int main(void) {
printf("start of main().\n");
if (setjmp(jump_buffer) == 0) {
/* 最初に setjmp() を呼び出したときは、こちらに分岐する */
printf("call top_function().\n");
top_function();
printf("returned from top_function().\n");
} else {
/* longjmp()から復帰したときは、こちらに分岐する */
printf("returned from longjmp().\n");
}
printf("end of main().\n");
return 0;
}
void top_function(void)
{
printf("start of top_function().\n");
middle_function();
printf("end of top_function().\n");
return;
}
void middle_function(void)
{
printf("start of middle_function().\n");
bottom_function();
printf("end of middle_function().\n");
return;
}
void bottom_function(void)
{
printf("start of bottlm_function().\n");
longjmp(jump_buffer, 1); /* setjmp() を呼び出した 関数 main() へ一足飛びに戻る */
printf("end of bottom_function().\n"); /* この処理が実行されることはない */
return;
}
1.2. サンプル・ソースコードの実行結果
start of main().
call top_function().
start of top_function().
start of middle_function().
start of bottlm_function().
returned from longjmp().
end of main().
- 関数 main() の中から top_function() を top_function() の中から middle_function() を middle_function() の中から bottom_function() を順に呼び出しています。
- 関数 main() の中で top_function() を呼び出していますが、次行の printf() を実行していません。
- 関数 top_function() の中で middle_function() を呼び出していますが、次行の printf() を実行していません。
- 関数 middle_function() の中で button_function() を呼び出していますが、次行の printf() を実行していません。
- 関数 buttom_function() の中で標準ライブラリ関数 longjmp() を呼び出していますが、次行の printf() を実行していません。
- 関数 buttom_function() の中で標準ライブラリ関数 longjmp() を呼び出した直後に main() 中のelse句を実行しています。
1.3. シーケンス図
1.3.1. return文
1.3.2. 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() を呼び出す条件を分岐すれば、ジャンプ処理/return文が実行されたり、されなかったりとなります。
2. 繰り返し処理の中でロングジャンプをつかった例
2.1. サンプル・ソースコード
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <time.h>
jmp_buf jump_buffer; /* ロングジャンプの復帰先スタックを保存するグローバル変数 */
void countdown(void);
void ignition(void);
void start_engine(void);
void lift_up();
int main(void)
{
if (setjmp(jump_buffer) == 0) {
/* setjmp() を最初に呼び出したときは、こちらに分岐する */
countdown();
printf("\n*** success! ***\n");
} else {
/* longjmp() から復帰してきたときは、こちらに分岐する */
printf("\n*** failed. ***\n");
}
return 0;
}
/*
* カウントダウンを実行し、各タイミングで ignition() と start_engine() と lift_up() を呼び出す関数
*/
void countdown(void)
{
for (int counter = 10 ; counter >=0 ; counter--) {
printf("%d.\n", counter);
switch (counter) {
case 5:
ignition();
break;
case 3:
start_engine();
break;
case 0:
lift_up();
break;
default:
; /* do nothing */
}
}
return;
}
void ignition(void)
{
printf("Ignition!\n");
return;
}
void start_engine(void)
{
/* 乱数を生成する */
srand((unsigned int)time(NULL));
int random_number = rand();
/* 乱数の偶奇で成功と失敗に分岐する */
if ((random_number % 2) == 0) {
printf("Start all engines!\n");
} else {
printf("Abort all engines!\n");
longjmp(jump_buffer, 1);
}
return;
}
void lift_up()
{
printf("Lift up!\n");
return;
}
2.2. サンプル・ソースコードの実行結果
10.
9.
8.
7.
6.
5.
Ignition!
4.
3.
Start all engines!
2.
1.
0.
Lift up!
*** success! ***
10.
9.
8.
7.
6.
5.
Ignition!
4.
3.
Abort all engines!
*** failed. ***
- 関数 start_engine() の中で乱数を生成し、longjmp()を実行したりしなかたり分岐しています。
- 関数 start_engine() の中でロングジャンプを実行しないときは、関数 countdown() の中の処理(含むforループ)を全て実行します。
- 関数 start_engine() の中でロングジャンプを実行したときは、関数 countdown() の中の続きの処理が丸っとスキップして、 main() に戻ります。
2.3. 解説
いわゆるエラーが発生したときに、以降の処理を丸っとスキップする流れが簡易に実現できます。この例では関数呼び出しが2階層と浅く、longjmp() を呼び出す箇所が一箇所であるため、ソースコードから全体の処理の流れを追跡するいことは比較的容易です。しかし、関数呼出しが4階層、5階層と深くなり、longjmp() を使ってジャンプする箇所が複数存在すると、メンテナンスやデバッグが難しいプログラムになります。
3. ロングジャンプの弊害
- プログラムに必要な後始末(たとえばOpen処理に対するClose処理)が漏れる(意図せずにスキップする)ミスを誘発します。
- ソースコードの『ここ』を通過するはず、とprintfデバッグを埋め込んだり、デバッガでブレイクポイントを仕掛けても、するっと通り抜けてしまいます。
Discussion