読者です 読者をやめる 読者になる 読者になる

libjpeg, libpngのエラー処理

libjpegやlibpngのエラー処理って、かなり醜いことになっていて、通常、Cだけでコーディングする場合には、setjmp/longjmpのお世話になってしまいます。で、そうすると、リソースリークなどの温床になることは必至で、個人的には大嫌いなものの一つです。こいつらのせいで、JPEG/PNGライブラリを自分で書いてやろうかとか思ってしまうぐらい嫌いです。

で、C++ならば当然、こういう場合には、例外を使ってすっきりと書けます!と言いたい感じになります。実際、JPEGエンコードに当たっては、

// エラー処理のためのお膳立て
class my_jpeg_error_mgr
{
public:
  my_jpeg_error_mgr(jpeg_compress_struct& cinfo)
  {
    cinfo.err = jpeg_std_error(&pub);
    pub.error_exit = my_jpeg_error_exit;
    pub.emit_message = my_jpeg_emit_message;
  }

private:
  jpeg_error_mgr pub;

  // エラー終了時にはこの関数が呼ばれる
  static void my_jpeg_error_exit(j_common_ptr cinfo)
  {
    char buffer[JMSG_LENGTH_MAX];
    (*cinfo->err->format_message)(cinfo, buffer);
    throw std::runtime_error(buffer);
  }

  // 警告時にはこの関数が呼ばれる
  static void my_jpeg_emit_message(j_common_ptr cinfo, int msg_level)
  {
    if(msg_level < 0)
      cinfo->err->num_warnings++; // increments warning count
  }
};

// ここからがエンコードのメイン
void encode_main()
{
  ...
  jpeg_compress_struct cinfo;
  my_jpeg_error_mgr jerr(cinfo);
  jpeg_create_compress(&cinfo);
  try
  {
    jpeg_xxx_dest_init(&cinfo);
    cinfo.image_width = (JDIMENSION)width;
    cinfo.image_height = (JDIMENSION)height;
    cinfo.in_color_space = JCS_RGB;
    cinfo.input_components = 3;
    jpeg_set_defaults(&cinfo);
    jpeg_start_compress(&cinfo, TRUE);
    for(size_t i = 0; i < cinfo.image_height; i++)
    {
      unsigned char* pLine = &lines[i];
      jpeg_write_scanlines(&cinfo, &pLine, 1);
    }
  }
  catch(std::exception& e)
  {
    // libjpegのクリーンナップだけを行い、例外は再送
    jpeg_destroy_compress(&cinfo);
    std::cout << e.what() << std::endl;
    throw;
  }

  // 正常終了
  jpeg_finish_compress(&cinfo);
  jpeg_destroy_compress(&cinfo);
}

というのがおきまりのパターンでしょう。で、このコードは、Windowsというか、Visual C++ならば何の問題もなく動作します。ところが、面倒なことに、普通のLinuxディストリビューションに付属のlibjpegをリンクすると、このコードのエラー処理は正しく動作しません。

gccのオプション

これは、通常、gccは、Cのプログラムをコンパイルするときに、例外フレーム(例外処理に必要ないろいろ)のないコードを生成するからです。簡単に言えば、C++なんてしらないよっていう感じのコードを生成します。そうすると、C++から、Cのコードを呼び出して、そのさきで、コールバック関数により、C++のコードに戻って例外を放つと、C++のcatchには戻ってこれません。一言で書くとわかりにくいので、簡単なコードを書くと、

/* exceptiontest.h */
#ifndef _exceptiontest_h_
#define _exceptiontest_h_

#ifdef __cplusplus
extern "C" {
#endif

typedef void (*CallbackFunc)(void);

void callCallbackFunc(CallbackFunc callbackFunc);

#ifdef __cplusplus
}
#endif

#endif /* _exceptiontest_h_ */
/* exceptiontest.c */
#include "exceptiontest.h"

void callCallbackFunc(CallbackFunc callbackFunc)
{
  // 素直にコールバックを呼び出すだけ
  callbackFunc();
}
// catchtest.cpp
#include <iostream>
#include <exception>
#include "exceptiontest.h"

// 例外を投げる関数
void exThrower()
{
  throw std::exception("!!!!");
}

int main()
{
  try
  {
    // 例外をC++ -> C -> C++のラウンドトリップさせる
    callCallbackFunc(exThrower);
  }
  catch(std::exception& e)
  {
    std::cout << e.what() << std::endl;
  }
}

ちょっと長いですが、概要は分かるかと。で、これを

gcc -c exceptiontest.c -o exceptiontest.o
g++ -c catchtest.cpp -o catchtest.o
g++ exceptiontest.o catchtest.o -o catchtest

としてビルドし、catchtestを実行すると何事もなく終わってしまいます。見事に例外がもみ消されます。

で、これは先ほど説明したことが起きているのですが、これに対処するオプションとして、

-fexceptions
Enable exception handling. Generates extra code needed to propagate exceptions. For some targets, this implies GCC will generate frame unwind information for all functions, which can produce significant data size overhead, although it does not affect execution. If you do not specify this option, GCC will enable it by default for languages like C++ which normally require exception handling, and disable it for languages like C that do not normally require it. However, you may need to enable this option when compiling C code that needs to interoperate properly with exception handlers written in C++. You may also wish to disable this option if you are compiling older C++ programs that don't use exception handling.

というものがあります。これを使うと、ビルドのコマンドラインは、

gcc -fexceptions -c exceptiontest.c -o exceptiontest.o
g++ -c catchtest.cpp -o catchtest.o
g++ exceptiontest.o catchtest.o -o catchtest

となります。これだけで、例外がちゃんと送出されるようになるわけです。しかしながら、逆に言えば、これだけのことのために、C++でlibjpeg/libpngを使用するためには、それらをソースからビルドしないといけなくなります。

そして、作ろうとしているのがライブラリだったりすると、さらなる不幸が起きるのですが、それはもう面倒きわまりない話になるのでここでは割愛させていただきます。