C言語のエラー処理: スタックトレースを作成する

321, 2021-09-13

目次

C言語のエラー処理~スタックトレース編

C言語には例外機構がありません。
そのためスタックトレースなども出力できませんが、C言語らしくスタックを自作することでスタックトレースを実現することが可能です。
この記事ではC言語で自力でスタックトレースを出力するということをやってみたいと思います。

この方法はいわゆる「こういう方法もあるよ」というエラー処理の一例であり、ベターであるかどうかは皆さんが判断してください。
ちなみに私はPadというプログラミング言語の開発でこのようなスタックトレース機構を使っています。
今のところ便利で、特にストレスはありません。

C言語のスタックトレースについて↓を見ていきたいと思います。

  • スタックの設計

  • 必要なヘッダのインクルード

  • 必要な定数の定義

  • 構造体スタックアイテムの定義

  • 構造体スタックの定義

  • エラーをプッシュする関数の定義

  • エラーがあるかチェックする関数の定義

  • スタックトレースを出力する関数の定義

  • スタックを実際に使う

  • 実行結果

  • コード全文

スタックの設計

エラーを格納するエラースタックの設計はどのような設計がいいでしょうか。
エラーが発生したときに知りたいのは↓のような部分です。

  • エラーが発生した場所

  • エラーの詳細

エラーが発生した場所というのは、つまりどのファイル、どの関数、どの行でエラーが起こったのかということです。
これらの情報がわかればエラーが発生した場所の特定が容易になります。
またエラーの詳細というのは、開発者が具体的に記述するエラー内容です。
つまりエラーが発生した際に開発者が記述するテキストで、これを文字列として保存しておく必要があります。

これらの情報は1つのエラーに対して必要です。つまり↑のような情報を1つのスタックアイテムとして定義します。
そしてスタックは、そのスタックアイテムの配列です。これは単純にスタックのデータ構造で作成します。

今回はスタックの長さは有限とし、一定数以上のアイテムがプッシュされた場合は「stack overflow」というエラーを出すようにします。
このエラー設計はあまりベターではありませんが、気になる方は変えてみてください。

また、今回はスタックは静的メモリで扱います。動的なメモリ確保などは使いません。
静的メモリにしたのは簡便さのためですが、取り扱いやすいように動的メモリ確保に変更するのも有りかと思います。

必要なヘッダのインクルード

エラースタックを定義するのに必要なヘッダファイルを最初にインクルードしておきます。

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

stdio.hprintf()などで使います。
またstdint.hint32_tなどの型を使うために使います。
stdbool.hbooltrue, falseを使うためにインクルードします。

必要な定数の定義

スタックやスタックアイテムの定義に必要な定数を定義しておきます。

enum {
    ERROR_STACK_ITEM_MSG_SIZE = 1024,
    ERROR_STACK_ITEMS_SIZE = 100,
};

スタックアイテムに保存されるエラーの詳細であるメッセージは、静的なcharの配列になります。
そのためERROR_STACK_ITEM_MSG_SIZEという定数を定義し、この配列のサイズを定数にしています。
また、今回はスタックのサイズは上限がありますが、これはERROR_STACK_ITEMS_SIZEとして定数にしておきます。

構造体スタックアイテムの定義

構造体でスタックアイテムを定義します。

typedef struct {
    const char *file_name;
    const char *func_name;
    long lineno;
    char message[ERROR_STACK_ITEM_MSG_SIZE];
} ErrorStackItem;

この構造体にはファイル名、関数名、行番号、そしてメッセージが保存されます。
それぞれfile_name, func_name, lineno, messageと定義しておきます。

スタックアイテムの構造体はtypedefErrorStackItemという型にしておきます。

構造体スタックの定義

つづいて本丸のエラースタックを定義します。

typedef struct {
    int32_t top;
    ErrorStackItem items[ERROR_STACK_ITEMS_SIZE];
} ErrorStack;

スタックの構造体はErrorStackという型にしておきます。
ErrorStackのメンバのitemsErrorStackItemの配列です。これはERROR_STACK_ITEMS_SIZE数分だけ確保しておきます。
そしてスタックの現在のポジションを表すtopを定義しておきます。

実際のエラー処理ではこのErrorStack型の変数を取りまわして使います。
関数の呼び出しなどではこの変数のポインタを第1引数に渡し使用します。

エラーをプッシュする関数の定義

次にエラーをプッシュする関数を作ります。

static void push_error(
    ErrorStack *stack,
    const char *file_name,
    const char *func_name,
    long lineno,
    const char *msg
) {
    if (stack->top >= ERROR_STACK_ITEMS_SIZE) {
        fprintf(stderr, "stack overflow\n");
        return;
    }

    ErrorStackItem *item = &stack->items[stack->top++];
    item->file_name = file_name;
    item->func_name = func_name;
    item->lineno = lineno;
    snprintf(item->message, sizeof item->message, "%s", msg);
}

push_error()は引数のデータをスタックにプッシュする関数です。
引数にはファイル名、関数名、行番号、メッセージを渡します。
関数の冒頭でtopをチェックして、これがERROR_STACK_ITEMS_SIZE以上だったらstack overflowを表示します。

topが範囲内だったら、スタックの配列からアイテムを1つ取得して、そのアイテムに引数のデータを保存します。
これと同時にtopを1つ進めておきます。

こうすることでスタックの配列にエラー情報が1単位でプッシュされます。
stack overflow周りのエラー処理は今回は省略していますが、stack overflowが発生したらbool値のfalseを返すとか、いろいろ設計は考えられます。

エラーがあるかチェックする関数の定義

スタックにエラーが存在するかチェックする関数を定義します。

static bool has_error(const ErrorStack *stack) {
    return stack->top != 0;
}

スタック内のエラーは、top0でなければ存在する(エラーがプッシュされている)ということになります。
そのため、↑のようにtop0でないかどうかチェックします。

スタックトレースを出力する関数の定義

スタックトレースを実際に出力する関数を定義します。

void stack_trace(ErrorStack *stack) {
    fprintf(stderr, "Stack trace:\n");

    for (int32_t i = 0; i < stack->top; i += 1) {
        ErrorStackItem *item = &stack->items[i];

        fprintf(stderr, "    %d: %s: %s(): %ld: %s\n",
            i, item->file_name, item->func_name,
            item->lineno, item->message);
    }
}

スタックトレースの出力では、スタックのアイテムの配列を先頭から参照していきます。
アイテムのfile_name, func_name, lineno, messageなどを参照して、stderrに出力します。

スタックを実際に使う

それではここまで作ってきたエラースタックを実際に使ってみます。
スタックは↓のように使います。

static void func(ErrorStack *stack) {
    push_error(stack, __FILE__, __func__, __LINE__, "猫の額ほど狭い");
}

int main(void) {
    ErrorStack stack = {0};

    push_error(&stack, __FILE__, __func__, __LINE__, "本日は晴天なり");
    func(&stack);
    push_error(&stack, __FILE__, __func__, __LINE__, "犬も歩けば棒に当たる");

    if (has_error(&stack)) {
        stack_trace(&stack);
    }

    return 0;
}

ErrorStack型の変数stackを1つ定義しておき、これを0クリアしておきます。
そしてpush_error()にその変数のアドレスを渡して、エラーをプッシュします。
func()など関数に入る場合は、関数の第1引数にスタックを渡して使います。
has_error()でスタックにエラーが無いかチェックして、エラーがあればスタックトレースを出力します。

push_error()に渡している__FILE____func____LINE__などは組み込みのマクロ変数です。
それぞれファイル名、関数名、行番号を取得できます。

push_error()がめんどくさい感じになっていますが、これはpush_errorをラップしたマクロ関数を1つ定義すれば解決します。たとえば↓のような感じです。

#define PUSH_ERROR(stack, msg) \
    push_error(stack, __FILE__, __func__, __LINE__, msg)

このマクロ関数は↓のように使うことが出来ます。

PUSH_ERROR(&stack, "エラーが発生!");

実行結果

ここまでのプログラムを実際に実行した結果は↓のようになります。

Stack trace:
    0: main.c: main(): 64: 本日は晴天なり
    1: main.c: func(): 58: 猫の額ほど狭い
    2: main.c: main(): 66: 犬も歩けば棒に当たる

スタックトレースが出力できていますね。

壮観だ

ふつくしい

コード全文

最後にコード全文を掲載します。

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

enum {
    ERROR_STACK_ITEM_MSG_SIZE = 1024,
    ERROR_STACK_ITEMS_SIZE = 100,
};

typedef struct {
    const char *file_name;
    const char *func_name;
    long lineno;
    char message[ERROR_STACK_ITEM_MSG_SIZE];
} ErrorStackItem;

typedef struct {
    int32_t top;
    ErrorStackItem items[ERROR_STACK_ITEMS_SIZE];
} ErrorStack;

static void push_error(
    ErrorStack *stack,
    const char *file_name,
    const char *func_name,
    long lineno,
    const char *msg
) {
    if (stack->top >= ERROR_STACK_ITEMS_SIZE) {
        fprintf(stderr, "stack overflow\n");
        return;
    }

    ErrorStackItem *item = &stack->items[stack->top++];
    item->file_name = file_name;
    item->func_name = func_name;
    item->lineno = lineno;
    snprintf(item->message, sizeof item->message, "%s", msg);
}

void stack_trace(ErrorStack *stack) {
    fprintf(stderr, "Stack trace:\n");

    for (int32_t i = 0; i < stack->top; i += 1) {
        ErrorStackItem *item = &stack->items[i];

        fprintf(stderr, "    %d: %s: %s(): %ld: %s\n",
            i, item->file_name, item->func_name,
            item->lineno, item->message);
    }
}

static bool has_error(const ErrorStack *stack) {
    return stack->top != 0;
}

static void func(ErrorStack *stack) {
    push_error(stack, __FILE__, __func__, __LINE__, "猫の額ほど狭い");
}

int main(void) {
    ErrorStack stack = {0};

    push_error(&stack, __FILE__, __func__, __LINE__, "本日は晴天なり");
    func(&stack);
    push_error(&stack, __FILE__, __func__, __LINE__, "犬も歩けば棒に当たる");

    if (has_error(&stack)) {
        stack_trace(&stack);
    }

    return 0;
}

おわりに

今回はC言語のエラー処理、スタックトレースについて実装してみました。
スタックトレースがあるとエラー時のデバッグが容易になります。
また、プログラムのエラーハンドリングをスタックで統一できるという利点もあります。

今回は静的なメモリ領域を使って実装しましたが、大規模なプログラムの開発では動的メモリ確保による実装などが必要になるでしょう。
もっとも、今の時代にC言語で大規模な開発を行っている所があるかはわかりませんが。

Linuxは大規模ですよ

Gtk+とかいろいろあるでしょ