ソフトウェア構造化例外処理3 [Except3]


1. ◆ 構造化例外処理の動作概要

構造化例外処理は、システムの異常な条件のために 処理を中断してユーザ・インターフェイスに復帰する記述を 容易にするなど様々な効果があります。

ハードウェア(CPU)の用語の『例外』とは異なるので注意してください。


キーワード:c_try, c_catch, c_end_catch, c_throw, Except3_init

Errors モジュールを使用したエラー発生(error)では、エラー終了することができても、 復旧(エラー処理)して続けて別の動作をさせることができません。 復旧させるには、ライブラリがエラーを発生する代わりに返り値でエラーが 発生したことを知らせるようにライブラリが復旧経路を作成するか、 構造化例外処理のシステムを使用するかどちらかになります。 本モジュール(Except3)は、後者の構造化例外処理システムを提供します。

復旧動作が要らないとき(システムをリセットする場合など)は、本モジュールは必要ありません。 復旧動作が必要な個所に構造化例外処理を記述します。

try ブロック(c_try) の内部(コール先を含む)で例外を投げる(c_throw) と、 catch ブロック(c_catch) にロングジャンプします。 try ブロックの外部(try を1つも記述していないときも含む)で 例外を投げると、Errors_exit 関数を呼び出してデフォルトの エラーメッセージを出力します(参照:Errors モジュールの 「エラーメッセージの確認方法 」)
try ブロックの内部で例外が投げられなかった場合、catch ブロックに 記述されたプログラムは実行しません。
例外を投げる記述(c_throw)はモジュールの独立性を考えて エラーハンドラの内部でのみ記述するようにします。 例外を投げると return 文のように、エラーハンドラ内の それ以降のプログラムは実行されません。
catch ブロックの内部では、エラーが発生しないようにしてください (詳細は「構造化例外処理の方針」を参照してください)。

C++ 言語でも使用可能です。ただし、C++ の throw を本モジュールの 例外処理ブロックの内部で発生させないようにしてください。 (ヒント:C 言語では、関数ポインタで C++ の関数を呼び出さない限り そのようなケースは起きません。C++ 言語では気を付けてください)


2. ◆ 構造化例外処理の実装手順と記述例

2-1. 記述の仕方

使用しているモジュールから発生したエラーから復帰できるように アプリケーションを作成する手順をまとめます。
以下のソースの動きは、「構造化例外処理の動作概要」 を参照してください。

  1. アプリケーション開始時(main 関数の最初)に、各種初期化関数 (Errors_MsgPool_init 関数、Except3_init 関数)と、 エラーハンドラ登録関数(Errors_setErrorHandler 関数)を 呼び出すようにする。(これらの記述は簡易的に済ませる こともできます。)
  2. エラーハンドラから例外を投げるときの記述例」 を参考にエラーハンドラを作成する。
  3. 構造化例外処理の動作概要」を参考に try 〜 catch ブロックを記述する。 または、「随時エラー確認スタイルでの構造化例外処理」 を記述する。

int  main()
{
  Errors_Msg  msgs[5];    /* エラーメッセージ領域 */
  Except3_Try  trys[10];  /* try スタック領域 */

  /* 構造化例外処理システムの初期化 */
  Errors_MsgPool_init( Errors_MsgPool_getGlobl(), msgs, sizeof(msgs) );
  Except3_init( trys, sizeof(trys) );
  Errors_setErrorHandler( err );  /* 参照:エラーハンドラから例外を投げるときの記述例 */

  /* メイン処理 */
  c_try {
    sub1( "a.txt" );
    sub1( "b.txt" );
  }
  c_catch ( Errors_Msg*, msg ) {
    Errors_Msg_print( msg );
  } c_end_catch;
}


void  sub1( char* path )  /* 参照:構造化例外処理に対応した関数の作成 */
{
  FILE*  file;
  bool  bFile = false:  /* file が有効かどうか */

  c_try {
    file = fopen( path, "rt" );
    if ( file == NULL )  error();  /* 参照:構造化例外処理の動作概要 */
    bFile = true;
       :
  }
  c_finally {
    if ( bFile )  fclose( file );
  } c_end_finally;
}

2-2. 簡易的な初期化の記述法

int  main()
{
  Except3_initEasy( Errors_stdErrHandler );
       :
int  main()
{
  Errors_Msg  msgs[5];    /* エラーメッセージ領域 */
  Except3_Try  trys[10];  /* try スタック領域 */

  /* 構造化例外処理システムの初期化 */
  Errors_MsgPool_init( Errors_MsgPool_getGlobl(), msgs, sizeof(msgs) );
  Except3_init( trys, sizeof(trys) );
  Errors_setErrorHandler( err );
       :


3. ◆ エラーハンドラから例外を投げるときの記述例

構造化例外処理を使用するプログラムにおけるエラーハンドラの記述例
int  err( int id, int code, char* msg )
{
  if ( code != Errors_ASSERT && code != Except3_Err_OutOfTry )
    c_throw( Errors_Msg_getGlobl() );

  return  ERRORS_EXIT;
}

テスト・プログラムにおけるエラーハンドラの記述例
int  err( int id, int code, char* msg )
{
  #ifdef  FOR_QUICK_TEST
    Errors_Msg_print( Errors_Msg_getGlobl() );
    if ( code != Except3_Err_OutOfTry )
      c_throw( Errors_Msg_getGlobl() );
  #endif

  id, code, msg;  /* avoid warning */

  return  ERRORS_EXIT;
}


4. ◆ 随時エラー確認スタイルでの構造化例外処理(返り値判定スタイル)

通常の構造化例外処理である try ブロック 〜 catch ブロックのスタイルでなくても、 返り値のエラーコードを確認するスタイルで記述することもできます。 ただし、例外が発生したかどうかを返り値で判断できないので、本モジュールが提供する 特別な if 文を使用します。

main()
{
  /* func 関数中に発生したエラーに対処する例 */
  c_if_error { x = func(); } c_then {   /* if の直後の中括弧に注意 */
    printf( "error form [func()]" );
  } c_endif;
}


5. ◆ 構造化例外処理に対応した関数の作成

構造化例外処理を正しくできるようにするには、関数ローカルで使用する (FILE* をスタックに作った)ファイルを クローズするなどの後始末処理を正しく行わなければなりません。
その場合は、finally ブロック(c_finally)を使用します。

参考:構造化例外処理の実装手順と記述例 sub1 関数


6. ◆ Except3 の構造化例外処理に対応していない関数の呼び出し

MFC など Except3 の構造化例外処理に対応していないライブラリを 使用して、そのライブラリを超える例外を投げた場合、 ライブラリが正しく動作しないことがあります。 その場合、ライブラリの関数を呼び出す前後に、 以下の関数を使用します。(下記のサンプルを参照)

ロングジャンプしない領域で例外を投げてもロングジャンプしません。 ただし、Except3_isError 関数は true を返すようになり、 Except3_endNoJumpArea 関数を呼び出して初めてロングジャンプします。
ロングジャンプしない領域の内部に try ブロックを作成した場合、 その中でロングジャンプしますが、その外はロングジャンプしません。 try ブロックのさらにその中で ロングジャンプしない領域を作成することもできます。

サンプル (行頭に * がついている部分は、 ロングジャンプで飛び越えません)
   /* ライブラリの関数を使用する関数 */
   void  level1()
   {
*    Except3_startNoJumpArea();
*    LibraryFunc();               /* LibraryCallback() へ */
*    Except3_endNoJumpArea();  /* 例外があればここからロングジャンプします */
   }

   /* LibraryFunc() からのコールバック関数 */
*  void  LibraryCallback()
*  {
*    ASSERT( Except3_isNoJumpArea() );  /* ロングジャンプしない領域かどうかチェック */
*    c_try {
       int  i = 0;

       c_throw();
     } c_finally {
       final();
*    } c_end_finally;
*
*    /* 例外が投げられてもここを実行します */
*    if ( Except3_isError() )  return  FALSE;  /* 例外があれば return FALSE する */
*    else  return  TRUE;
*  }

関連:『構造化例外処理に対応した関数の作成』


7. ◆ 例外発生時のデバッグ

返り値でエラーコードを判断するプログラミング・スタイルと異なり、 例外が発生するといきなり catch ブロックにロングジャンプするので、 ユーザープログラムからエラーが発生した位置を特定することが 面倒になりますが不可能ではありません。
例外を投げる(c_throw)と exit と同様にそれ以降の(正常動作の)プログラムが 実行されないことを利用して、プログラムがどこまで実行されたかを printf や MARK マクロ(Errors モジュール)を使用して確かめることで、 どこでエラーが発生したかを特定することができます。

関数コールスタックを表示できるデバッグ環境の場合、 エラーメッセージに含まれるエラーID を確認し、 エラーハンドラ内でエラーID を比較して ブレークするようにします。

上の2つのデバッグ方法はどちらもコンパイルする必要があるので、 サイズが大きいソースファイルをコンパイルする場合は、 非効率的になります。 また、コンパイルすることでプログラムサイズが変化し、 ハードウェアに対するタイミングが変わってしまうことがあるので、 再現性を失う可能性があります。 デバッグ時にこのようなことが起きないか心配な場合は、 バグが現れる前にあらかじめ 随時エラー確認スタイルでの構造化例外処理 を行ってください。


8. ◆ 構造化例外処理の方針

例外が発生したときに対処方法は、大きく2つに分れます。

  1. 以後の処理は無効なので処理をしない
  2. 例外を無視して処理を続ける
  3. 対処すべき例外でない場合は対処しない
1.は try 〜 finally タイプ、2.は try 〜 catch タイプ、 3.は try { try 〜 catch 〜 throw_again } finally タイプの、 構造化例外処理を行います。

エラーが発生した位置を確認するには、次の方法があります。

  1. 例外が発生した場所にブレークポイントを張って、 Errors モジュールの COUNT マクロを使用して、 デバッガの関数コールのスタックウォッチ機能を使って確認する
  2. Except3_exit_imp 関数の Errors_exit 文にブレークポイントをつけて、 デバッガのスタックウォッチ機能を使って関数コールの様子を確認する


9. ◆ 構造化例外処理のテスト

全ての例外を発生させることは、現実的には不可能です。

例外が発生したときのテストケースは、try ブロックの構成を参考に モジュール構成ツリーの最も深いところから例外を発生させます。

テストプログラムのエラー時に例外を投げ、エラー発生後に 次のテストを続いて行うようにすれば、 ブラックボックステスト的にテストしたことになります。


written by Masanori Toda from Dec.21.1998