C言語の標準ライブラリ関数 setjmp() と longjmp() を呼び出すことで多段の関数呼出階層を飛び越えるジャンプ(いわゆるGOTO処理)を実現できます。しかしながら、現代的なプログラミングでは GOTO文 が忌避されるように、setjmp() と longjmp() を使ったジャンプは推奨されません。やむを得ず setjmp() と longjmp() で実装された既存のソースコードを理解するための助けとなることを目論んだ解説です。
- 多段の関数呼出階層
- 関数の中から下位の関数を呼び出して、その下位の関数の中から下位の下位の関数を呼び出して、その下位の下位の関数の中から下位の下位の下位の関数を呼び出す、といった構造
1. 単純なロングジャンプの例
1.1. サンプル・ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
#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. サンプル・ソースコードの実行結果
1 2 3 4 5 6 7 |
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階層ずつ呼出元へ戻ったばあい
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() を呼び出す条件を分岐すれば、関数から抜け出る手順(戻っていく先)がマチマチにあります。もしくはマチマチにすることを目的にして longjmp() を利用します。
2. 繰り返し処理の中でロングジャンプをつかった例
2.1. サンプル・ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
#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. サンプル・ソースコードの実行結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
10. 9. 8. 7. 6. 5. Ignition! 4. 3. Start all engines! 2. 1. 0. Lift up! *** success! *** |
1 2 3 4 5 6 7 8 9 10 11 12 |
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デバッグを埋め込んだり、デバッガでブレイクポイントを仕掛けても、するっと通り抜けてしまいます。