📚

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

2023/03/26に公開

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().
  1. 関数 main() の中から top_function() を top_function() の中から middle_function() を middle_function() の中から bottom_function() を順に呼び出しています。
    1. 関数 main() の中で top_function() を呼び出していますが、次行の printf() を実行していません。
    2. 関数 top_function() の中で middle_function() を呼び出していますが、次行の printf() を実行していません。
    3. 関数 middle_function() の中で button_function() を呼び出していますが、次行の printf() を実行していません。
  2. 関数 buttom_function() の中で標準ライブラリ関数 longjmp() を呼び出していますが、次行の printf() を実行していません。
  3. 関数 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. ***
  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デバッグを埋め込んだり、デバッガでブレイクポイントを仕掛けても、するっと通り抜けてしまいます。

Discussion