TRACEマクロの作り方
こんにちは、高木です。
最近関わっている仕事では、あえてCを使ってコーディングする機会が増えてきています。
とくにマイコンのプログラミングとかでは、貧弱なライブラリしか用意されていないケースも多く、自分でいろいろな関数やマクロを自作しなければなりません。
今回は、決して目新しい内容ではないのですが、ベテランのプログラマーでも意外に知らなかったりするTRACEマクロの作り方について書いてみることにします。
ここでいうTRACEマクロはいわゆるprintfデバッグのためのもので、printfと同じ書式が使えて、なおかつリリース時には無効化できるものです。
まず、最初に立ちはだかる難関は、可変個引数の扱いのようです。
可変個引数は、分かってしまえば簡単なことなのですが、機会に恵まれなければ一生使うことがないかもしれません。
可変個引数を扱うには、ヘッダをインクルードする必要があります。
そして、va_listという型と、va_start, va_arg, va_endというマクロを使います。
今回はprintfと同じ書式を使えるようにするため、sprintfでいったん文字列に変換することを考えます。
最近はC99が普通に使えるようになってきましたので、sprintfよりは安全なsnprintfを使うほうがいいでしょう。
ただ、可変個引数を受け取って、それをそっくりそのまま可変個引数として別の関数に渡すことはできません。
そこで、snprintfではなくvsnprintfという関数を使うことにします。
実際のコードは次のようになります。
1 2 3 4 5 6 7 8 9 10 | void trace(const char *format, ...) { char s[1024]; va_list ap; va_start(ap, format); vsnprintf(s, sizeof(s), format, ap); /* sを出力 */ va_end(ap); } |
実際の出力をどうするかはプラットフォームによります。
標準出力や標準エラーへの出力であればfputs関数とかでよいでしょうし、Windowsのデバッガ出力であればOutputDebugStringA関数になるでしょう。
マイコンであればシリアルポート等への出力になると思います。
ここで、バッファとして用意したsは結構大きなサイズを自動記憶域(要するにスタック上)に確保しています。
マイコンのようにスタックが小さい場合には、これはちょっと許容できないかもしれません。
静的に確保してもよいのですが、その場合はtrace関数がリエントラントではなくなりますので、別途排他制御が必要になります。
次に、いよいよこれをマクロにして、リリース時には無効化できるようにします。
リリース時かどうかは、assertマクロと同様、NDEBUGマクロが定義されていればリリース時と判断することにしましょう。
1 2 3 4 5 | #ifndef NDEBUG #define TRACE(...) trace(__VA_ARGS__) #else #define TRACE(...) ((void)0) #endif |
これで、NDEBUGマクロが定義されていなければトレース出力が行われますが、NDEBUGマクロを定義すればトレース出力を無効にすることができます。
可変個引数マクロはC99以降の機能ですので、C90を使う場合はちょっと工夫が必要になります。
1 2 3 4 5 | #ifndef NDEBUG #define TRACE trace #else #define TRACE (void)sizeof #endif |
このようにしておけば、たとえば TRACE(“%d\n”, value) と書いた場合、NDEBUGマクロを定義すると (void)sizeof(“%d\n”, value) に展開されますので、value変数などは評価されることがありません。
C99の可変個引数マクロが使えるのであれば、もう一工夫することで、ファイル名と行番号もいっしょに出力することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void trace(const char* file, int line, const char *format, ...) { char s[1024]; va_list ap; int n; va_start(ap, format); n = snprintf(s, sizeof(s), "%s:%d: ", file, line); vsnprintf(s + n, sizeof(s) - n, format, ap); /* sを出力 */ va_end(ap); } #ifndef NDEBUG #define TRACE(...) trace(__FILE__, __LINE__, __VA_ARGS__) #else #define TRACE(...) ((void)0) #endif |
このようにすれば、先頭に「ファイル名:行番号: 」が付加されることになります。
大切なことは、ファイル名や行番号と、マクロに引数で指定した内容を、ひとつの文字列にした上で一気に出力する必要があることです。
そうしなければ、マルチタスクの環境ではトレース出力の途中に、別のタスクによるトレース出力が割り込んでしまうことがあります。
もちろん、一気に出力することでそうした問題が解決するのは、プラットフォーム依存の出力処理内部で排他制御が行われていることが前提になります。
そうではない場合、あるいは前述したように大きな自動変数を確保できない場合、自分で排他制御する必要が出てきます。
こういった便利なマクロや関数を手早く実装できるようになっておくと、何かと重宝するものです。