ライブラリでのエラー処理とジャンプ


くますけ劇場PNGが表示できないのはイタいよなー、PNGも壁紙にしたりしたいなーとか前から思っていたので、最近はずっとPNGローダを作ってました(といってもlibpngを使った簡単なやつですが)。
ネットや昔の雑誌やらでサンプルコードをかき集め、TP上ではなんとか表示できるようになったのですが、そのコードはほとんどサンプルを切り張りして作ったものであり、コードの中には自分でよく理解できてない部分も多く「とりあえず動いてる」感じだったので、いまいち気分もすっきりしませんでした。
で、PNGが表示できるようになってからやった最初のことは、「リージョンを切る」ことでした。これも前からやってみたかったことなのですが、雑誌のサンプルコードをほとんどそのまま使って実際にリージョンが切れたときは、感動でした。まさに「立った、立ったよ!」という感じです(何が)。こんなの見ちゃったらやっぱり「しゃべらせたくなるよなー」とか思いながらいろんな画像のリージョン切らせてしばらく遊んでました。
で、これを改造して簡単な壁紙チェンジャでもつくろうかと思ったのですが、壁紙チェンジャつくるならJPEGもサポートしたいよね、ということでJPEG ローダにも挑戦しました。くますけ劇場は現在JPEGを開けるのですが、あれはDirectShowが勝手に開いてくれるので私は何もしてないし、たかが JPEG開くだけのことにDirectX必須にするのはいかにもアレなので、ゆくゆくはくまきちランチにそんな機能を追加することも考えつつ、JPEGローダに挑戦したわけです。
で、jpeglib(IJG)に関する情報をこれまた雑誌の記事やらネットからかき集め、これもなんとか表示できました。
さて、ここからが今日の本題なのですが、JPEGライブラリのサンプルを見ていると、見たことのあるコードに気付きました。こんなやつ。

    cinfo.err = jpeg_std_error(&jerr.pub);

    jerr.pub.error_exit = my_error_exit;

    if (setjmp(jerr.setjmp_buffer)) {

    	jpeg_destroy_decompress(&cinfo);

    	fclose(infile);

    	return 0;

    }

    jpeglib添付のsample.cより引用(コメントは削除)


実はこれと似たようなコードがlibpngのサンプルにもあって、この赤字で示した命令がその時はよく意味がわからず「なにやってんだろ。この辺」とか思ってたんですね。setjmpのドキュメント見ても「スタック環境を保存し……」とか書いてあるし、「この時代にスタックて……マシン語じゃないんだから……恐ッ」と、その先の内容も、コードのコメントの英文もろくに読みもせず理解することから逃げていました。
で、このJPEGライブラリを眺めているうちにわかったのは、標準のエラーハンドラがあるということ、そして、そこで使われているのがなんと「exit」(即座にプログラムを終了)ということでした。つまりライブラリ関数内でなんかエラーが起きた時などに、エラーハンドラへジャンプするようになってるんですが、そこへ来た時点でプログラムがストップ(終了)するわけです。
これでは使えないじゃないか、というわけで、参考にさせてもらったある方のコードでは、この標準エラーハンドラ内部を書き換え、無理やり例外を起こすようにしていました(アドレス0番地に0を書き込む)。そしてライブラリ関数を利用する部分にまるごとtry/exceptかまして事なきを得ていたのですが、これはこれであまりそのまま真似したくはなかったので、なんか他の方法があるんじゃないかと探しているうちに見つけたコードが上に引用した部分でした。
上のコードは何をしているかというと、「標準のエラーハンドラに換わる別途用意した自作のエラーハンドラをつくり、それをライブラリがエラーハンドラとして認識するよう設定」しています(my_error_exitが自作のエラーハンドラ用関数)。ちなみにサンプルのエラーハンドラ自体はこんなやつでした。

    struct my_error_mgr {

      struct jpeg_error_mgr pub;	/* "public" fields */

      jmp_buf setjmp_buffer;	/* for return to caller */

    };

    typedef struct my_error_mgr * my_error_ptr;



    METHODDEF(void)

    my_error_exit (j_common_ptr cinfo)

    {

      my_error_ptr myerr = (my_error_ptr) cinfo->err;

      (*cinfo->err->output_message) (cinfo);

      longjmp(myerr->setjmp_buffer, 1);

    }

    jpeglib添付のsample.cより引用(主なコメントは削除)


要するに、これと同じような関数を自分で用意してやれば、エラー処理を自分で定義できるわけです。標準エラーハンドラを書き換える必要もなく、こちらのほうがやり方としてはスマートなので、こっちを採用することにしました。そして、ここでやっと引用部の赤字の部分で何をしていたかに合点がいきました。
つまり、最初の引用部の赤字の部分ではエラーハンドラから帰ってくるコード位置(スタック等)を保存し、実際にエラーハンドラに来た時には2つ目の引用部の赤字のところでスタックを復帰=保存した場所へジャンプして戻る、ということをしていたのです(エラーハンドラから戻ったあとは、最初の引用部のif文内のCleanUpコードを実行してreturnする)。
そもそも、setjmpのヘルプをよく読めばちゃんと「setjmp 関数と longjmp 関数では、非ローカル goto を実行できます。」(VC++6.0添付のMSDNより引用)と書いてあるのでつまりはそういうことだったんですが、これを理解した時、鳥肌が立ちました。非ローカルのジャンプ! そんなおそろしいことができたのか! という感じで。
昔のBASICとかであれば、ローカルも糞もなかったので、好き放題ジャンプしてたりしたわけですが、Cでもこういうことってできるんだなあといまさらながら知って驚きました。


という話。




参考にさせていただいた資料とか
PNG ProgrammingResources
libpngソースコード
FreeImageソースコード(PluginPNG.cpp)
Independent JPEG Group jpeglibのソースコード
WindowsCE プログラミング TIPS 第3〜8回
C Magazine』 1999 10月号 「ライブラリ活用術」