C
エラー処理

Cで行なう自作モジュールのエラー処理 - スタックトレース

 Cでは例外機構が標準に備わっていないが、setjmpやlongjmpなどを使うと似たような機構を再現できるらしい。今回はその機構は使わずに、errというオブジェクトを仲介することでエラー時のスタックトレースを実現するようにしてみた。

 errというエラー捕捉用のオブジェクトを定義し、testmodなどの自作モジュールから参照させる。testmod内でエラーが起きたらエラーの詳細をerrにプッシュし、それを呼び出し毎に繰り返し、最終的に上層のレイヤーでスタックトレースを実現する。

 全てのモジュールでerrが参照される想定なので、errの作法に沿ってコーディングを行なう必要がある。つまりerrの設計に破綻があったら全体的に( ˘ω˘)スヤァ

 errは呼び出し毎に状態が変わる可能性があるので、使う前にerr_resetで状態を初期化しているが、errnoと似た手間を感じる。

 今回は実装していないが、err_pushをマクロ化し、__FILE__や__LINE__なども事象に保存すれば更に便利なエラー出力を得られそう。


test-err.c

#include <stdio.h>

#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <ctype.h>

/*******
* util *
*******/

/**
* メッセージを出力して安らかに死ぬ( ˘ω˘)スヤァ
*/

void
die(const char *fmt) {
fflush(stdout);
fprintf(stderr, "%s", fmt);
fflush(stderr);
exit(1);
}

/***********
* errevent *
***********/

/**
* ユーザーに使用してもらうエラー定数。
* 例外の種類に相当。
*/

enum {
ERR_NOERR = 0,
ERR_INVALID_ARG,
ERR_FAIL_MALLOC,
ERR_RUNTIME,
ERR_VALUE,
};

enum {
ERREVENT_WHAT_SIZE = 1024,
};

/**
* エラーの事象を記録する構造体。
*/

struct errevent {
int no;
char what[ERREVENT_WHAT_SIZE];
};

/**
* 事象の状態をリセットする。
*/

void
errev_reset(struct errevent *self) {
if (!self) {
die("self is null");
}

self->no = ERR_NOERR;
self->what[0] = '\0';
}

/**
* 事象を設定する。
*/

void
errev_set(struct errevent *self, int no, const char *what) {
if (!self || !what) {
die("invalid arguments");
}

self->no = no;
snprintf(self->what, sizeof self->what, "%s", what);
}

/**
* エラー定数を文字列にする。
*/

const char *
errev_notomsg(const struct errevent *self) {
if (!self) {
die("self is null");
}

switch (self->no) {
case ERR_NOERR: return "no error"; break;
case ERR_INVALID_ARG: return "invalid argument"; break;
case ERR_FAIL_MALLOC: return "failed to malloc"; break;
case ERR_RUNTIME: return "runtime error"; break;
case ERR_VALUE: return "value error"; break;
}

return "unknown error";
}

/**
* 事象を出力する。
*/

void
errev_show(const struct errevent *self) {
if (!self) {
die("self is null");
}

fprintf(stderr, "%s. %s.\n", errev_notomsg(self), self->what);
}

/******
* err *
******/

enum {
ERR_STACK_SIZE = 64,
};

/**
* エラースタックを管理する構造体。
*/

struct err {
int stack_top;
struct errevent stack[ERR_STACK_SIZE];
};

/**
* selfがNULLなら安らかに死ぬ( ˘ω˘)スヤァ
*/

void
err_checknull(struct err *self) {
if (self) {
return;
}

die("err is null");
}

/**
* エラースタックの状態をリセットする。
*/

void
err_reset(struct err *self) {
if (!self) {
die("self is null");
}

self->stack_top = 0;
errev_reset(&self->stack[0]);
}

/**
* エラースタックに事象をプッシュする。
*/

void
err_push(struct err *self, int no, const char *what) {
if (!self || !what) {
die("invalid argument");
}

if (self->stack_top >= ERR_STACK_SIZE) {
die("err stack overflow");
}

errev_set(&self->stack[self->stack_top], no, what);
self->stack_top += 1;
}

/**
* エラースタックが事象を持っているならtrue
*/

bool
err_haserr(const struct err *self) {
if (!self) {
die("self is null");
}

return self->stack_top > 0;
}

/**
* スタックトレースを出力する。
*/

void
err_stacktrace(struct err *self) {
if (!self) {
die("self is null");
}

for (int i = 0; i < self->stack_top; ++i) {
errev_show(&self->stack[i]);
}
}

/**********
* testmod *
**********/

/**
* テスト用モジュール。
*/

struct testmod {
char name[128];
};

/**
* コンストラクタ。
*/

struct testmod *
testmod_new(struct err *err, const char *name) {
err_checknull(err);

if (!name) {
err_push(err, ERR_INVALID_ARG, "name of argument is null");
return NULL;
}

struct testmod *self = calloc(1, sizeof(*self));
if (!self) {
err_push(err, ERR_FAIL_MALLOC, "failed to calloc");
return NULL;
}

snprintf(self->name, sizeof self->name, "%s", name);

return self;
}

/**
* 調整した名前を取得する。
*/

void
testmod_fix_name(struct err *err, struct testmod *self, char *dst, size_t dstsize) {
err_checknull(err);

if (!self || !dst) {
err_push(err, ERR_INVALID_ARG, "invalid arguments");
return;
}

if (strcmp(self->name, "invalid name") == 0) {
err_push(err, ERR_VALUE, "invalid name value");
return;
}

memmove(dst, self->name, strlen(self->name)+1);

if (islower(dst[0])) {
dst[0] = toupper(dst[0]);
}
}

/**
* 装飾した名前を取得する。
*/

void
testmod_decolate_name(struct err *err, struct testmod *self, char *dst, size_t dstsize) {
err_checknull(err);

if (!self || !dst) {
err_push(err, ERR_INVALID_ARG, "invalid arguments");
return;
}

char fixname[128];
testmod_fix_name(err, self, fixname, sizeof fixname);
if (err_haserr(err)) {
err_push(err, ERR_RUNTIME, "failed to fix name");
return;
}

snprintf(dst, dstsize, "★*.+ %s *.+★", fixname);
}

/**
* デストラクタ。
*/

void
testmod_del(struct err *err, struct testmod *self) {
err_checknull(err);

if (!self) {
err_push(err, ERR_INVALID_ARG, "self is null");
return;
}

free(self);
}

/*******
* main *
*******/

void
test(const char *name) {
struct err _err, *err = &_err;

err_reset(err);
struct testmod *testmod = testmod_new(err, name);
if (err_haserr(err)) {
err_push(err, ERR_RUNTIME, "failed to testmod new");
err_stacktrace(err);
return;
}

char decname[128];

err_reset(err);
testmod_decolate_name(err, testmod, decname, sizeof decname);
if (err_haserr(err)) {
err_push(err, ERR_RUNTIME, "failed to testmod decolate name");
err_stacktrace(err);
return;
}

puts(decname);

err_reset(err);
testmod_del(err, testmod);
if (err_haserr(err)) {
err_push(err, ERR_RUNTIME, "failed to testmod del");
err_stacktrace(err);
return;
}
}

int
main(void) {
puts("---- test1 ----");
test("taro");

puts("---- test2 ----");
test("invalid name"); // エラー発生用の名前

return 0;
}

/* Result.

$ gcc err.c -std=c11 && ./a.out
---- test1 ----
★*.+ Taro *.+★
---- test2 ----
value error. invalid name value.
runtime error. failed to fix name.
runtime error. failed to testmod decolate name.
*/