C言語でJavaと同等程度の例外処理をやってみる

ご存じの通りC言語には例外処理という立派なものは搭載されていない。
そこで、C言語でsetjmpという関数を使ってJavaと同等の例外処理をやってみる。

JavaとCでmy_divという関数を作って例外処理をする

  • int型の引数を2つ持つ関数(メソッド)を定義して割り算した結果をintで返す
  • a / bのときbがゼロの場合にゼロ除算例外を起こす
  • a / bのときaがゼロの場合にその他の例外を起こす

Javaでは・・

bがゼロの時は特に例外判定をする必要がなく、勝手にゼロ除算例外になるので次のようになる。

class Longjump {
	public static void main(String[] args) {
		int x, y, ans;

		if (args.length != 2) {
			System.err.println("Usage: java Longjump X Y");
			System.exit(1);
		}
		x = Integer.parseInt(args[0]);
		y = Integer.parseInt(args[1]);

		try {
			ans = my_div(x, y);
			System.out.println(x + " / " + y + " = " + ans);
		} catch (ArithmeticException e) {
			System.err.println("Zero div exception!");
		} catch (Exception e) {
			System.err.println("Exception!");
		}
	}

	private static int my_div(int a, int b) throws Exception, ArithmeticException {
		if (a == 0) {
			throw new Exception();
		}
		return a / b;
	}
}

結果

$ java Longjump 10 3
10 / 3 = 3

$ java Longjump 10 0
Zero div exception!

$ java Longjump 0 3
Exception!

C言語では・・

setjmpとlongjmpのコンボでこのようになる

#include <stdio.h>
#include <setjmp.h>

enum {
	ZERO_DIV_EXCEPTION = 1,
	OTHER_EXCEPTION    = 2,
};

jmp_buf env;

int my_div(int a, int b) { /* throws ZERO_DIV_EXCEPTION */
	if (a == 0) {
		/* throw OTHER_EXCEPTION;*/
		longjmp(env, OTHER_EXCEPTION);
	}
	if (b == 0) {
		/* throw ZERO_DIV_EXCEPTION;*/
		longjmp(env, ZERO_DIV_EXCEPTION);
	}

	return a / b;
}

int main(int argc, char *argv[]) {
	int x, y, ans, e;

	if (argc != 3) {
		fprintf(stderr, "Usage: ./longjmp X Y\n");
		return 1;
	}
	x = atoi(argv[1]);
	y = atoi(argv[2]);

	if ((e = setjmp(env)) == 0) { /* try */
		ans = my_div(x, y);
		printf("%d / %d = %d\n", x, y, ans);
	} else if (e == ZERO_DIV_EXCEPTION) { /* catch (ZERO_DIV_EXCEPTION e) */
		fprintf(stderr, "Zero div exception!\n");
	} else { /* catch (Exception e) */
		fprintf(stderr, "Exception!\n");
	}

	return 0;
}

結果

$ ./longjmp 10 3
10 / 3 = 3

$ ./longjmp 10 0
Zero div exception!

$ ./longjmp 0 3
Exception!

おお、まったく同じ結果になった。
しかも綺麗!!

仕組み

setjmp

setjmpは呼ばれた時の実行コンテキスト(プログラムカウンタ(CP)を含む汎用レジスタの値など)がjmp_bufに格納される。
※longjmpが呼ばれた後以外は、必ず0が返却される。

longjmp

longjmpは第一引数に指定したjmp_bufに格納される実行コンテキストの状態へ遷移するようになっている。
※第二引数は、setjmpへ遷移した時のsetjmpの戻り値となる(第二引数に0を指定した場合は強制的に1に変更される)

以上の仕組みにより、関数呼び出しを行っていた場合でもsetjmpが呼ばれた位置まで何事もなく戻ってくる。

注)
Javaでも同じだが、例外が起こるまでに変更されたメモリ内容
例えば

if ((e = setjmp(env)) == 0) { /* try */
	ans = 999;
	ans = my_div(x, y);
	printf("%d / %d = %d\n", x, y, ans);
}

となる場合には、my_divで例外が起こってもansの値は999となったままで
メモリの値まで戻らないという事を注意してもらいたい。

まとめ

setjmpはRuby等の言語処理系で例外を実現するため等にも使用されており、とても使える手法である。
ぜひ、自分も含めて覚えておきたい1手だ。

他どんな時に使われることが多いのだろうか?