2012年8月21日火曜日

例外機構 (exception mechanism)

オブジェクト指向とは直接関係ないが、言語 Cで例外の実験をしてみたいと思う。
(実験用のため実用にするときには十分な検討が必要。使用上の制約もあることにも留意。)

たとえば、次のようなコードを動かしたい。
try {
                throw(new_TestException("test"));
        } catch (TestException, e) {
                printf("TestException caught, %s\n", e->message);
        } finally {
                printf("finally\n");
        }
構造はつぎのようになる。
try
        主成功シナリオ (main success scenario)
catch
        代替シナリオ (alternative scenario extensions)
finally
        tryブロック終了処理 (always executes when the try block exits)
C++には finallyが無くて困ったことがあるので、なんとか実現したい。
catch節の TestExceptionの後ろに付いている ',' (comma)は間違って付けた鼻くそではない。
Cに無い構文 try - catch - finallyのためにマクロを使おうと思うが、catchは引数つきマクロにするため、型名とインスタンス名の2つを引数を区切る ','が必要となる。

マクロ定義 (macro definition)

例外機構のためのヘッダファイル throw.hを記述してみた。
/*
 * 9.11.12.13.29.31.Dec.2011 kei
 * 15.16.21.Aug.2012
 */

#ifndef _Included_lwd_throw
#define _Included_lwd_throw

#include <setjmp.h>
#include <lwd/Environment.h>

#define lwd_try \
        auto void lwd_Environment_finally(); \
        { \
        struct lwd_Environment lwd_exception; \
        lwd_Environment_push(&lwd_exception); \
        int lwd_jumpStatus = setjmp(lwd_exception.buffer); \
        if (lwd_jumpStatus == 0) {

#define lwd_catch(T, V) \
        } else if (strcmp(lwd_Throwable_getType(lwd_exception.throwable), #T) \
        == 0) { \
                lwd_boolean lwd_Environment_catchClause = lwd_true; \
                lwd_Environment_pop(&lwd_exception); \
                if (!lwd_Environment_isEmpty() \
                && (lwd_exception.throwable != NULL)) \
                        lwd_Environment_setThrowable(&lwd_exception); \
                T V = (T)lwd_exception.throwable; \

#define lwd_finally \
        } else { \
                lwd_Environment_pop(&lwd_exception); \
                if (!lwd_Environment_isEmpty() \
                && (lwd_exception.throwable != NULL)) \
                        lwd_Environment_setThrowable(&lwd_exception); \
                lwd_throw(lwd_exception.throwable); \
        }} \
        lwd_Environment_finally(); \
        void lwd_Environment_finally()

#define lwd_throw(O) \
        if (lwd_Environment_catchClause) \
                lwd_Environment_finally(); \
        if (lwd_Environment_isEmpty()) \
                lwd_Environment_abort((lwd_Throwable)O); \
        lwd_Environment_throw((lwd_Throwable)O)

#endif /* _Included_lwd_throw */

Cで try - catchを実現するほかの多くの例と同様、setjmp, longjmpを使用する。
try, catch, finally, throwは各々 lwd_try, lwd_catch, lwd_finally, lwd_throwとして定義している。
これを、lwd.hファイルの中で置き換えている。
#define try     lwd_try
        #define catch   lwd_catch
        #define finally lwd_finally
        #define throw   lwd_throw

finallyを実現するために、GCC拡張の ローカル関数(関数の中で定義される関数)を使用している。
auto void lwd_Environment_finally()がそれにあたる。
catchの中では、catchClause変数を trueとすることで、外にあるときと throwの動作を切り替えている。
throwが呼ばれたときに直ちに上位階層に longjmpしてしまっては finallyが実行されなくなる。catchClauseが trueの場合は longjmpに先立って finally関数が呼ばれる。ただしこのfinally関数内で throwが行われると、そちらが優先されてしまう。
try 以外のところで throwを実行すると、abortする。

環境 (environment)

setjmp, longjmpのためのコンテキストを保存するために、Environmentクラスを使用している。

Environment.hはつぎのとおり。
/*
 * 9.12.13.29.31.Dec.2011 kei
 * 13.Jan.2012
 * 21.Aug.2012
 */

#ifndef _Included_lwd_Environment
#define _Included_lwd_Environment

#include <setjmp.h>

typedef struct lwd_Environment* lwd_Environment;

#include <lwd/EnvironmentSPI.h>
#include <lwd/Throwable.h>
#include <lwd/types.h>

struct lwd_Environment {
        jmp_buf buffer;
        lwd_Environment chain;
        lwd_Throwable throwable;
};

extern void lwd_Environment_push(lwd_Environment environment);

extern void lwd_Environment_pop(lwd_Environment environment);

extern lwd_boolean lwd_Environment_isEmpty();

extern void lwd_Environment_setThrowable(lwd_Environment environment);

extern const lwd_boolean lwd_Environment_catchClause;

extern void lwd_Environment_throw(lwd_Throwable throwable);

extern void lwd_Environment_finally();

extern void lwd_Environment_abort(lwd_Throwable throwable);

extern void lwd_Environment_setSPI(lwd_EnvironmentSPI spi);

#endif /* _Included_lwd_Environment */

bufferがレジスタなどのコンテキストを保存するための領域。
chainはtryの中でさらに tryを使用する場合にスタック構造をつくるためのもの。
throwableは実際にスローされた例外。
push, pop, isEmptyは tryが多重に呼ばれたときの環境を操作するための関数。
catchClauseは tryの中にいるかどうかを判断するために使用される名称、外部は常に falseになる。
setSPIは、push, pop等の動作を変更したい場合に使用する。

関数定義の Environment.cはつぎのとおり。
/*
 * 9.10.12.13.29.31.Dec.2011 kei
 * 13.Jan.2012
 * 21.Aug.2012
 */

#include <lwd/Environment.h>

#include <lwd/throw.h>
#include <stdio.h>
#include <stdlib.h>

/****************************************************************
 private
****************************************************************/

static lwd_Environment lwd_environment = (void*)0;

static void push(lwd_Environment environment) {
        environment->chain = lwd_environment;
        environment->throwable = NULL;
        lwd_environment = environment;
}

static void pop(lwd_Environment environment) {
        lwd_environment = environment->chain;
}

static lwd_boolean isEmpty() {
        return (lwd_environment == NULL);
}

static void setThrowable(lwd_Environment environment) {
        lwd_environment->throwable = environment->throwable;
}

static struct lwd_EnvironmentSPI singleThreadSPI = {
        push,
        pop,
        isEmpty,
        setThrowable
};

static lwd_EnvironmentSPI environmentSPI = &singleThreadSPI;

/****************************************************************
 public
****************************************************************/

const lwd_boolean lwd_Environment_catchClause = lwd_false;

void lwd_Environment_push(lwd_Environment environment) {
        environmentSPI->push(environment);
}

void lwd_Environment_pop(lwd_Environment environment) {
        environmentSPI->pop(environment);
}

lwd_boolean lwd_Environment_isEmpty() {
        return environmentSPI->isEmpty();
}

void lwd_Environment_setThrowable(lwd_Environment environment) {
        environmentSPI->setThrowable(environment);
}

void lwd_Environment_throw(lwd_Throwable throwable) {
        lwd_environment->throwable = throwable;
        longjmp(lwd_environment->buffer, 1);
}

void lwd_Environment_finally() {
}

void lwd_Environment_abort(lwd_Throwable throwable) {
        printf("uncaught exception '%s' occured.\n",
                lwd_Throwable_getType(throwable));
        exit(1);
}

void lwd_Environment_setSPI(lwd_EnvironmentSPI spi) {
        environmentSPI = (spi != NULL)
                ? spi
                : &singleThreadSPI;
}

デフォルトの SPIはシングルスレッド用である。
マルチスレッドに対応するためには、使用するスレッド機構(例えば pthread)用の SPIを記述し、差し替える必要がある。
abort関数の中で、printfを使用しているが、これは手抜き。fprintfで stderrに出力すべきだ。

テスト (test)

簡単なテストケース ExceptionExampleTest.cをつぎに示す。
/* 15.Aug.2012 kei */

#define import_lwd_unit_CUnit

#include <lwd/lwd.h>
#include <lwd/throw.h>
#include <lwd/Throwable.h>
#include <lwd/unit/CUnit.h>
#include <stdio.h>

lwd_Environment lwd_environment = NULL;

typedef struct TestException* TestException;

struct TestException {
        struct lwd_Throwable throwable;
        char* message;
};

TestException new_TestException(char* message) {
        TestException this =
                (TestException)malloc(sizeof(struct TestException));
        if (this != NULL) {
                lwd_Throwable_Throwable((lwd_Throwable)this, "TestException");
                this->message = message;
        }
        return this;
}

void testException() {
        try {
                throw(new_TestException("test"));
        } catch (TestException, e) {
                printf("TestException caught, %s\n", e->message);
        } finally {
                printf("finally\n");
        }
}

int main(int argc, char** argv) {
 return CUnit_test(argc, argv);
}

実行結果は、つぎの通り。
$ ./ExceptionExampleTest
[CUnit] ./ExceptionExampleTest: testException
TestException caught, test
finally
[SUCCEED] testException in 2(ms)
successes:    1 (100.0%)
failures:     0 (0.0%)
errors:       0 (0.0%)
total:        1 in 2(ms)
$ 

ごっそり、秘密をあげるわ。

0 件のコメント:

コメントを投稿