C言語への感謝の正拳突き 今日は10日目です!
こんなの書いてる暇あったら進捗すすめてくださいという声もあるなか、
試験勉強中は部屋の掃除が捗るとかそういうことなんだと思います。。
今回もOSSを読んでいて勉強になったマクロの使い方を紹介したいと思います。
macroとの出会い
C言語の勉強がてらCで書かれたOSSを探しては
とりあえずgrepとかでdefineマクロを探してしまいます。
仮にlinuxのソースコード(linux-4.3.1/include/linux)で実行してみると、
66,184行程度見つかります、こんなの見てらんない。。
大半のケースにおいて、綺麗なマクロの使い方をするOSSが多いなか、
ときどきですが、おっ!?というマクロの定義と、使い方をするものがあり、大変勉強になります。
たとえばh2oです。
h2oの例
#define H2O_STRUCT_FROM_MEMBER(s, m, p) ((s *)((char *)(p)-offsetof(s, m)))
offsetofマクロに関してはこちらのページが詳しい
http://www5d.biglobe.ne.jp/~noocyte/Programming/CMacros.html#StructBase
どういうケースで使われているかというと、たとえばですね。
typedef struct st_h2o_timeout_entry_t h2o_timeout_entry_t;
typedef void (*h2o_timeout_cb)(h2o_timeout_entry_t *entry);
/**
* an entry linked to h2o_timeout_t.
* Modules willing to use timeouts should embed this object as part of itself, and link it to a specific timeout by calling
* h2o_timeout_link.
*/
struct st_h2o_timeout_entry_t {
uint64_t registered_at;
h2o_timeout_cb cb;
h2o_linklist_t _link;
};
timeout_entryとtimeout_cbはいろいろなところで使いまわせる汎用的な定義になっており、
複数の構造体のメンバになっています。
この宣言を見たとき、timeout_cbの引数、timeout_entryしかないじゃん、
親contextかuser_dataなくてもいいんだっけ?と疑問に思ったのですが。
struct st_h2o_http1_conn_t {
h2o_conn_t super;
h2o_socket_t *sock;
/* internal structure */
h2o_timeout_t *_timeout;
h2o_timeout_entry_t _timeout_entry; <-- timeout_entryを起点にst_h2o_http1_conn_tのアドレスを計算
...
/* the HTTP request / response (intentionally placed at the last, since it is a large structure and has it's own ctor) */
h2o_req_t req;
};
static void reqread_on_timeout(h2o_timeout_entry_t *entry)
{
struct st_h2o_http1_conn_t *conn = H2O_STRUCT_FROM_MEMBER(struct st_h2o_http1_conn_t, _timeout_entry, entry); <-- h2o_timeout_cbとして定義された関数の中で、親のcontextを引き当ててtimeout時の処理をする
/* TODO log */
conn->req.http1_is_persistent = 0;
close_connection(conn);
}
引数で受けったcontextをvoid*から目的の型にキャストするより、
変なコード書いてもコンパイル時に怒られて良いかも。。今度試してみようかなと思っちゃいました。
大変勉強になります!
#grpcの例
grpcのcore部分はC言語で書かれており、
マルチプラットフォームなイベント駆動のサンプルとしては素晴らしいじゃないでしょうか。
もちろんdefineマクロをさらっと眺めて面白そうなものを探してみました。
grpc/src/core/transport/metadata.c
/* Reference counting */
#ifdef GRPC_METADATA_REFCOUNT_DEBUG
#define GRPC_MDSTR_REF(s) grpc_mdstr_ref((s), __FILE__, __LINE__)
#define GRPC_MDSTR_UNREF(s) grpc_mdstr_unref((s), __FILE__, __LINE__)
#define GRPC_MDELEM_REF(s) grpc_mdelem_ref((s), __FILE__, __LINE__)
#define GRPC_MDELEM_UNREF(s) grpc_mdelem_unref((s), __FILE__, __LINE__)
grpc_mdstr *grpc_mdstr_ref(grpc_mdstr *s, const char *file, int line);
void grpc_mdstr_unref(grpc_mdstr *s, const char *file, int line);
grpc_mdelem *grpc_mdelem_ref(grpc_mdelem *md, const char *file, int line);
void grpc_mdelem_unref(grpc_mdelem *md, const char *file, int line);
#else
#define GRPC_MDSTR_REF(s) grpc_mdstr_ref((s))
#define GRPC_MDSTR_UNREF(s) grpc_mdstr_unref((s))
#define GRPC_MDELEM_REF(s) grpc_mdelem_ref((s))
#define GRPC_MDELEM_UNREF(s) grpc_mdelem_unref((s))
grpc_mdstr *grpc_mdstr_ref(grpc_mdstr *s);
void grpc_mdstr_unref(grpc_mdstr *s);
grpc_mdelem *grpc_mdelem_ref(grpc_mdelem *md);
void grpc_mdelem_unref(grpc_mdelem *md);
#endif
参照カウント用のref/unrefメソッドを呼び出す際に、デバッグ用にFILE/LINEを付加してデバッグするのか、
面白いなーって思ってたら、、引数増えてるんですけど。。
#define DEBUG_ARGS , const char *file, int line // <-- __FILE__と__LINE__用の引数追加
#define FWD_DEBUG_ARGS , file, line
...
grpc_mdstr *grpc_mdstr_ref(grpc_mdstr *gs DEBUG_ARGS) { //<-- 仮引数増やしている
internal_string *s = (internal_string *)gs;
grpc_mdctx *ctx = s->context;
lock(ctx);
internal_string_ref(s FWD_DEBUG_ARGS); //<-- 更に次の関数に渡している
unlock(ctx);
return gs;
}
void grpc_mdstr_unref(grpc_mdstr *gs DEBUG_ARGS) {
internal_string *s = (internal_string *)gs;
grpc_mdctx *ctx = s->context;
lock(ctx);
internal_string_unref(s FWD_DEBUG_ARGS);
unlock(ctx);
}
DEBUG_ARGSで引数fileとlineを継ぎだしているようでした。
それをさらにFWD_DEBUG_ARGSってマクロで更に次の関数に渡している。
#ifdef GRPC_METADATA_REFCOUNT_DEBUG
#define DEBUG_ARGS , const char *file, int line
#define FWD_DEBUG_ARGS , file, line
#define INTERNAL_STRING_REF(s) internal_string_ref((s), __FILE__, __LINE__)
#define INTERNAL_STRING_UNREF(s) internal_string_unref((s), __FILE__, __LINE__)
#define REF_MD_LOCKED(s) ref_md_locked((s), __FILE__, __LINE__)
#else
#define DEBUG_ARGS
#define FWD_DEBUG_ARGS
#define INTERNAL_STRING_REF(s) internal_string_ref((s))
#define INTERNAL_STRING_UNREF(s) internal_string_unref((s))
#define REF_MD_LOCKED(s) ref_md_locked((s))
#endif
static void internal_string_ref(internal_string *s DEBUG_ARGS) {
#ifdef GRPC_METADATA_REFCOUNT_DEBUG
gpr_log(file, line, GPR_LOG_SEVERITY_DEBUG, "STR REF:%p:%d->%d: '%s'", s, // <-- ここでfile, lineを参照
s->refs, s->refs + 1, grpc_mdstr_as_c_string((grpc_mdstr *)s));
#endif
++s->refs;
}
__FILE__と__LINE__をマクロの中に記述しておいて、
マクロが展開されたソースコード上で評価してデバッグするみたいのは時々やりますが、
ref/unrefのデバッグで使っているのは初めてみました。
※昔はmalloc/freeも似たようなことやってた時期があるんですけど、最近はclang asanとかで漏れなく検証できる。。
__FILE__や__LINE__マクロは、展開された箇所のソースコードを参照するため、
うまくマクロで展開するために、大量にマクロを書いてたりすることもあるのですが、
引数をマクロで増やして次々渡していけば、関数のネストが深くても巧くFILE,LINEが参照できるのかー、
なるほどーと思いました。
大変勉強になります!
他にこんなのもありました。
/* Provide unwrapping macros because we're in C89 and variadic macros weren't
introduced until C99... */
#define GRPC_API_TRACE_UNWRAP0()
#define GRPC_API_TRACE_UNWRAP1(a) , a
#define GRPC_API_TRACE_UNWRAP2(a, b) , a, b
#define GRPC_API_TRACE_UNWRAP3(a, b, c) , a, b, c
#define GRPC_API_TRACE_UNWRAP4(a, b, c, d) , a, b, c, d
#define GRPC_API_TRACE_UNWRAP5(a, b, c, d, e) , a, b, c, d, e
#define GRPC_API_TRACE_UNWRAP6(a, b, c, d, e, f) , a, b, c, d, e, f
#define GRPC_API_TRACE_UNWRAP7(a, b, c, d, e, f, g) , a, b, c, d, e, f, g
#define GRPC_API_TRACE_UNWRAP8(a, b, c, d, e, f, g, h) , a, b, c, d, e, f, g, h
#define GRPC_API_TRACE_UNWRAP9(a, b, c, d, e, f, g, h, i) \
, a, b, c, d, e, f, g, h, i
#define GRPC_API_TRACE_UNWRAP10(a, b, c, d, e, f, g, h, i, j) \
, a, b, c, d, e, f, g, h, i, j
/* Due to the limitations of C89's preprocessor, the arity of the var-arg list
'nargs' must be specified. */
#define GRPC_API_TRACE(fmt, nargs, args) \
if (grpc_api_trace) { \
gpr_log(GPR_INFO, fmt GRPC_API_TRACE_UNWRAP##nargs args); \
}
C99のvar-argの代わりに力技でマクロ展開するっぽい、、C89かー、まぁーこういうのは使う機会が来ないといいな。。
#まとめ
(1) callbackの見通しを良くするマクロの使い方と、FILE __LINE__を関数呼び出しがネストしたとこに巧いこと伝搬する方法を紹介しました。
(2) CのOSSはこういう楽しみ方もあるんですよ。
(3) Modern C++で書かれたOSSの場合、define macroを探したりしません。
このOSS(で定義されているマクロ)は素晴らしい!みたいな話あれば教えてください。
以上