はじめに
本記事は電子書籍版もあります。
linuxカーネルはC言語のマクロを駆使して書かれています。それらのうち、凝ったマクロになじみの無い人には初見では意図がわからない&わかってみれば面白いであろうものをいくつか紹介いたします。対象読者は、C言語のユーザだけれども、マクロは定数定義くらいにしか使わないというライトなマクロユーザです。
マクロを使用する場所に依存するエラーを防ぐ
次のマクロは、二つの引き数の値を置換するだけの単純なものです。
#define swap(a, b) \ do { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } while (0)
注目すべきはマクロの定義全体を囲んでいるdo { ... } while (0)
という表記です。初見の人には何のことかわからないと思います。考えられる最も単純な定義から遡って、なぜこのような定義にするとよいのかを見てみましょう。
このマクロのdo {} while文のブロックを外したバージョンのマクロを使ってみましょう。
#include <stdio.h> #define swap(a, b) \ typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; int main(void) { int a = 0, b = 1; swap(a, b); printf("%d %d\n", a, b); return 0; }
実行例を示します。
$ make swap cc swap.c -o swap $ ./swap 1 0 $
ちゃんと動いているように見えます。しかし、これが次のような使い方だといかがでしょうか。
#include <stdio.h> #define swap(a, b) \ typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; int main(void) { int a = 0, b = 1; if (0) swap(a, b); printf("%d %d\n", a, b); return 0; }
コンパイルします。
$ make swap2 cc swap2.c -o swap2 swap2.c: In function 'main': swap2.c:4:9: error: expected expression before 'typeof' typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; ^ swap2.c:10:3: note: in expansion of macro 'swap' swap(a, b); ^~~~ swap2.c:4:49: error: '__tmp' undeclared (first use in this function) typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; ... make: *** [swap2] Error 1 $
期待値はif文の中のswap()マクロは実行せずに端末上に"0 1¥n"という出力をする、というものですが、実際は山ほどエラーが出てコンパイルが失敗しました。ソースをコンパイルせずにプリプロセッサだけをかけて原因を探ってみましょう。
$ cc -E swap2.c ... int main(void) { int a = 0, b = 1; if (0) typeof(a) __tmp = (a); (a) = (b); (b) = __tmp;; printf("%d %d\n", a, b); return 0; } $
一見正しいように見えますが、制御構造を意識して整形してみると、おかしい点がわかってきます。
int main(void) { int a = 0, b = 1; if (0) typeof(a) __tmp = (a); (a) = (b); (b) = __tmp;; printf("%d %d\n", a, b); return 0; }
swap()マクロ内の3つの命令のうち、一行目の一時変数__tmpを宣言している行はif文の中にありますが、それ以外の2命令はif文の外に出てしまっています。これではまともに動くはずがありません。さらに、if文の中に変数宣言のみを1行置くことは許されないので、上記コンパイルログの一行目のようなエラーが出ています。
では次のようにマクロ定義を単にブロック("{}")で囲めばいいのではないか、というかたもいらっしゃるかと思うので、試してみます。
#include <stdio.h> #define swap(a, b) \ { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } int main(void) { int a = 0, b = 1; if (0) swap(a, b); printf("%d %d\n", a, b); return 0; }
$ make swap3 cc swap3.c -o swap3 $ ./swap3 0 1 $
こちらはうまくいきました。しかしこれは次のようなケースではうまくいきません。
#include <stdio.h> #define swap(a, b) \ { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } int main(void) { int a = 0, b = 1; if (0) swap(a, b); else printf("Always print this message¥n"); printf("%d %d\n", a, b); return 0; }
$ make swap4 cc swap4.c -o swap4 swap4.c: In function 'main': swap4.c:11:2: error: 'else' without a previous 'if' else ^~~~ <builtin>: recipe for target 'swap4' failed make: *** [swap4] Error 1 $
期待値は"Always print this message¥n"の後に"0 1¥n"が出力される、なのですが、謎のコンパイルエラーが発生しました。これについてもプリプロセッサによる処理後のソースを見てみましょう。
$ cc -E swap4.c ... # 6 "swap4.c" int main(void) { int a = 0, b = 1; if (0) { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; }; else printf("Always print this message¥n"); printf("%d %d\n", a, b); return 0; }
さきほどと同様に、制御構造を意識してソースを整形します。
int main(void) { int a = 0, b = 1; if (0) { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; }; # ... (1) else printf("Always print this message"); printf("%d %d\n", a, b); return 0; }
ややわかりにくいのですが、ソース内の(1)のところでCの構文上if文は完結しています。したがって、それに続くelse節はコンパイラから見るとif文無しに突然出てきたように見えるため、エラーが出ていたのでした。
この場合はswap(a,b);
の末尾のセミコロンを省けばうまく動作します。しかしこれは明らかに直感的ではないので、このような使い方はできれば避けたいです。上記の命令列を単なるブロックではなく do {} while (0)で囲めば、それが可能になります。コード例は出しませんが、この場合は上記すべての場合についてうまく動作します。
上記のような「うまくいかないケース」を全て知らないと、なかなかこの do {} while (0) の意図は理解できないと思います。linuxカーネル以外でも頻出のCマクロのイディオムなので、覚えておいて損はないと思います。
ジェネリックプログラミング
さきほどのswap()の例をもう一度見てみましょう。
#define swap(a, b) \ do { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } while (0)
これはインライン関数で実装しても同じように見えますが、実際やってみると面倒なことがわかります。以下のコードを見て下さい。
static inline void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; }
これはこれで動くのですが(引数に変数でなく変数へのポインタを指定しないといけないところは異なります)、このswap()はintにしか使えません。別の型については別のswap()を定義する必要があります。しかも、Cは関数のオーバーロード機能1が無いため、複数の型に対するswap()を同時に定義したい場合は、例えば次のように定義する必要があります。
static inline void swap_int(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } static inline void swap_double(double *a, double *b) { int tmp = *a; *a = *b; *b = tmp; }
呼び出すためにいちいち型名を指定する必要がある上に、同じような意味のコードを重複して書く必要があるので保守性が非常に悪いです。マクロを使えばこのような問題を避けられます。ちょうどC++のテンプレートを使ったジェネリックプログラミングのようなことができます。
ビルドの設定に応じて何もしない関数/マクロを定義する
linuxカーネルでは、特定のビルド設定において、特定の関数を何もしないように定義している箇所が多々あります。次に示す実際のコードを見てみましょう。
... #if BITS_PER_LONG==32 && defined(CONFIG_SMP) #include <linux/seqlock.h> #define __NEED_I_SIZE_ORDERED #define i_size_ordered_init(inode) seqcount_init(&inode->i_size_seqcount) #else #define i_size_ordered_init(inode) do { } while (0) #endif ...
このコード断片は、ぱっと見ややこしそうですが、言葉で説明すると次のようなことをしています。
- i_size_ordered_init()というマクロを定義する
- ビルド対象アーキテクチャのlongのサイズが32であり、かつ、マルチプロセッサ環境であればseqcount_init()を呼ぶ
- そうでなければ何もしない
注目してもらいたいのはi_size_ordered_init()マクロのdo { } while (0)
という定義です。これは先程の例の応用で、「何もしない」関数/マクロを定義しています。
このマクロを呼び出している箇所でいちいち
... { ... #if BITS_PER_LONG==32 && defined(CONFIG_SMP) i_size_ordered_init(); #endif ... } ...
などとするよりはるかにコードの保守性が高いです。
なお、#define i_size_ordered_init(inode)
(マクロ定義を空にする)や、#define i_size_ordered_init(inode) {}
などという定義にすると、前述のようなさまざまなコーナーケースが存在してしまいます。
引数の文字列化
次は、マクロの引数を文字列にする方法について学んでみましょう。例として、以下のlinuxカーネル内のコードを示します。
... #ifdef CONFIG_SCHED_DEBUG #define SCHED_WARN_ON(x) WARN_ONCE(x, #x) #else ... #endif ...
ここでは簡単のためCONFIG_SCHED_DEBUGが定義されていると考えて、SCHED_WARN_ON()マクロが何をするものなのかを見ていきます。このマクロは、スケジューラのコードの中で、スケジューラが異常な状態であることを示す条件を満たした(満たしてしまった)ときにカーネルのログに、どの条件文が成立したかを示す警告メッセージを表示するためのものです。
SCHED_WARN_ON()の中で使われているWARN_ONCE()マクロは、第一引数に指定された条件が満たされたときに、第二引数に指定されたデバッグ用メッセージを出力します2。
SCHED_WARN_ON()を素直に実装、使用しようとすると次のようになります(実際のものとは異なります)。
#define SCHED_WARN_ON(x, msg) WARN_ONCE(x, msg) ... { ... SCHED_WARN_ON(number_of_runnanble_processes < 0, "number_of_runnable_processes < 0"); ... } ...
これで一応目的を達成できるのですが、一見してわかるように、なんだかダサいです。同じテキスト("number_of_runnable_processes < 0")を二回書かなくてはいけないため、書くのが面倒な上に、条件を変えたときにメッセージの追従を忘れたりする可能性があり、保守性が悪いです。これを避けるのがCマクロの、引き数の文字列化機能です。
引数の文字列化機能は、マクロの引数の前に"#"という演算子を付けることによって実現します。たとえば#define tokenize(a) #a
とマクロを定義すると、tokenize(test)
は"test"
と評価されます。上記の実際のSCHED_WARN_ON()は、これを応用して、第一、そして唯一の引数に条件文を渡すことで、当該条件を満たした際に、条件式を示す文字列をログに出力できます。
実際の使用例は次の通りです。
... #define SCHED_WARN_ON(x) WARN_ONCE(x, #x) ... static inline struct cpuidle_state *idle_get_state(struct rq *rq) { SCHED_WARN_ON(!rcu_read_lock_held()); return rq->idle_state; } ...
cpuidle_state()関数の定義はプリプロセッサによって次のように変換されます。
static inline struct cpuidle_state *idle_get_state(struct rq *rq) { WARN_ONCE(!rcu_read_lock_held(), "!rcu_read_lock_held()"); return rq->idle_state; }
上記の素直な実装例よりはるかに書くのが楽で、かつ保守性の高いコードになることがわかります。
トークンの連結
Cのマクロ定義の中では、2つのトークン3の連結によって新たなトークンを生成できます。これは文字列の連結とは全く異なります。以下のサンプルコードをごらんください。
#define concat_token(a) \ static int func_##a(void) \ { \ return 0; \ } concat_token(foo) int main(void) { return func_foo(); }
先頭のconcat_token()マクロの定義の中のfunc_##a
という箇所に注目してください。これは"func_"というトークンと、引数aで示したトークンの2つを連結したトークンを作るという意味です。多分意味不明だと思うので、実例を見てみましょう。
conat_token(foo)を評価した場合、func_##a
はfunc_foo
になります。その後、マクロ全体の評価結果は次のようになります。
static int func_foo(void) \ { \ return 0; \ }
ソース全体をプリプロセッサにかけてみましょう。
$ cc -E concat_token.c ... static int func_foo(void) { return 0; } int main(void) { return func_foo(); } $
func_foo()という関数が定義されていることがわかります。つまりこのマクロは、引き数に指定したトークン(ここでは"foo")を含む関数を定義するものであることがわかります。
これだけでは用途がわかりにくいので、linuxカーネル内の使用例を見てみましょう。
... #define EXT4_FEATURE_COMPAT_FUNCS(name, flagname) \ static inline bool ext4_has_feature_##name(struct super_block *sb) \ { \ return ((EXT4_SB(sb)->s_es->s_feature_compat & \ cpu_to_le32(EXT4_FEATURE_COMPAT_##flagname)) != 0); \ } \ static inline void ext4_set_feature_##name(struct super_block *sb) \ { \ EXT4_SB(sb)->s_es->s_feature_compat |= \ cpu_to_le32(EXT4_FEATURE_COMPAT_##flagname); \ } \ static inline void ext4_clear_feature_##name(struct super_block *sb) \ { \ EXT4_SB(sb)->s_es->s_feature_compat &= \ ~cpu_to_le32(EXT4_FEATURE_COMPAT_##flagname); \ } ...
一見複雑ですが、実はやっていることは単純です。これはext4ファイルシステム内の各機能(mkfs.ext4(8)やtune2fs(8)の-Oオプションによって有効/無効を設定)に関する関数を一括定義するためのマクロです。第一引数nameが示す機能について、第二引数flagnameによって示すフラグを操作する、一連の関数を定義します。
例えば次のように使用します。
EXT4_FEATURE_COMPAT_FUNCS(dir_prealloc, DIR_PREALLOC)
これは次のように展開されます。
... static inline bool ext4_has_feature_dir_prealloc(struct super_block *sb) \ { \ return ((EXT4_SB(sb)->s_es->s_feature_compat & \ cpu_to_le32(EXT4_FEATURE_COMPAT_DIR_PREALLOC)) != 0); \ } \ static inline void ext4_set_feature_dir_prealloc(struct super_block *sb) \ { \ EXT4_SB(sb)->s_es->s_feature_compat |= \ cpu_to_le32(EXT4_FEATURE_COMPAT_DIR_PREALLOC); \ } \ static inline void ext4_clear_feature_dir_prealloc(struct super_block *sb) \ { \ EXT4_SB(sb)->s_es->s_feature_compat &= \ ~cpu_to_le32(EXT4_FEATURE_COMPAT_DIR_PREALLOC); \ }
上記3つの関数の定義はそれぞれ次の通りです。
- ext4_has_feature_dir_prealloc: 引数sbで指定したext4ファイルシステムがdir_prealloc機能4を持っているかどうかを判定
- ext4_set_feature_dir_prealloc: 同機能を有効化
- ext4_clear_feature_dir_prealloc: 同機能を無効化
一見3つの関数をマクロ内で定義するなどという回りくどいことをせずに直接定義したほうが簡単そうに見えますが、同じような定義が何度も続くような場合にこのマクロは大きな威力を発揮します。実際、ext4のdir_prealloc以外の様々な機能について同様な定義が必要であり、それぞれについて上記のEXT4_FEATURE_COMPAT_FUNCS()マクロ5で一括定義しています。これによって膨大な量の機械的なつまらないコーディングを減らせます。
EXT4_FEATURE_COMPAT_FUNCS(dir_prealloc, DIR_PREALLOC) EXT4_FEATURE_COMPAT_FUNCS(imagic_inodes, IMAGIC_INODES) EXT4_FEATURE_COMPAT_FUNCS(journal, HAS_JOURNAL) EXT4_FEATURE_COMPAT_FUNCS(xattr, EXT_ATTR) EXT4_FEATURE_COMPAT_FUNCS(resize_inode, RESIZE_INODE) EXT4_FEATURE_COMPAT_FUNCS(dir_index, DIR_INDEX) EXT4_FEATURE_COMPAT_FUNCS(sparse_super2, SPARSE_SUPER2)
トークンの連結は一見便利そうですが、cscopeなどのツールが、マクロによって生成される変数や関数をうまく認識してくれずに、ソースコードリーディングが面倒になるなどという欠点もあります。cscopeなどを使って関数やマクロの定義を探しても全く出てこないという場合は、##演算子を使って定義されたものであるかどうかを疑ってみるとよいと思います。
構造体のフィールドから、それを埋め込んだ親構造体へのポインタを得る
次に示すのは、ある構造体のフィールドから、それを埋め込んでいる親構造体へのポインタを得るマクロです。
/** * container_of - cast a member of a structure out to the containing structure * @ptr: the pointer to the member. * @type: the type of the container struct this is embedded in. * @member: the name of the member within the struct. * */ #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );})
ptr(第一引数)がmember(第三引数)というフィールド名で埋め込まれたtype(第二引数)型のデータのアドレスを求めます。まずは、どうやってこのような機能を実装しているかを、これから紐解いていきます。
container_of()の中にあるoffsetof()の定義を示します6。
... #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) ...
このマクロによって、TYPE(第一引数)で示される構造体の中のMEMBER(第二引数)フィールドのバイト単位のオフセットが求められます。このマクロは、ゼロ番地に配置したTYPE型データの中のMEMBERフィールドのアドレス(をsize_t型にキャストしたもの)はTYPE内のMEMBERのオフセットに等しいという性質を利用しています。わかってしまえば簡単なのですが、初見ではけっこう意味不明で引いてしまうかもしれません。
これを踏まえてcontainer_of()の定義を再度見てみましょう。
... #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );})
一行目はちょっと回りくどく見えますが、単に mptr変数にptr(第一引数)を代入しているだけです。二行目では mptr(すなわちptr)から、type内のmemberのオフセットを引いています。つまり、これでptrの埋め込み元であるtype型のデータが求まってしまうということです。一見一行目を省いて二行目を(type *)((char *)ptr - offsetof(type,member))
だけにすれば済みそうに見えますが、一行目によって、ptrとmemberの型の対応が取れていない場合に警告メッセージが出るようになっており、思わぬバグを防げるようになっています。
例によって、これだけでは何が嬉しいのかよくわからないので、linuxカーネルのファイルシステムのコードを実例として紹介します。
linuxカーネルにおいて、ファイルシステムのコードはVirtual File System層(以後VFS層と記載)という全ファイルシステム(ext4, XFS, Btrfsなど)共通のコードと、各ファイルシステム固有のコードに分かれています。たとえば全ファイルシステムに共通するinodeに関する情報はVFS層に存在するstruct inodeという構造体によって表現します。これに対して、各ファイルシステムは、自身固有のinode情報を含む構造体を持っており、その中にstruct inodeを埋め込んでいます。
Btrfsを例にとって説明すると、btrfs固有のinode情報はstruct btrfs_inode構造体に格納されます。そのうちファイルシステム共通の部分、つまりさきほど述べたstruct inodeは、この構造体の中のvfs_inodeというフィールドとして埋め込まれています。
... struct btrfs_inode { ... struct inode vfs_inode; }; ...
Btrfs内のinodeの各種時刻([cma]time)を更新する際は、VFS層からbtrfs_update_time()という関数が呼ばれます。
static int btrfs_update_time(struct inode *inode, struct timespec *now, int flags) { struct btrfs_root *root = BTRFS_I(inode)->root; ... }
この関数のインターフェイスはBtrfsを含む個々のファイルシステムではなくVFS層によって定義されていますので、その引き数によって渡されるinode情報は必然的にstruct btrfs_inodeではなく、struct inodeになります。しかし、Btrfsとしては時刻の更新に伴って後者だけではなく前者の情報を使って処理をする必要があります。
ではどうすればいいかというと、ここでcontainer_of()が登場します。btrfs_update_time()冒頭のBTRFS_I()の中で、container_of()を呼び出すことによって、inode(第一引数)をvfs_inode(第三引数)というフィールド名で埋め込んでいるstruct btrfs_inode(第二引数)のアドレスを求めます。
static inline struct btrfs_inode *BTRFS_I(struct inode *inode) { return container_of(inode, struct btrfs_inode, vfs_inode); ... }
後は求めたstruct btrfs_inodeのデータへのポインタを使って粛々と処理をするだけです。具体的にどういう処理をするかは本書の対象範囲外なので割愛します。
リスト操作
linuxカーネルは、その中にstruct list_headという構造体によって管理する双方向リストの実装を持っています。このリストは例によって、C言語のマクロを最大限に活用して実装されています。この節ではその実装について扱います。
リストについての基本的な知識は、お手数ですが別記事の"リストの構造"という節に書いていますので、そちらを参照してください。短いし単純なので、短時間で読めると思います。
リストを処理するためのマクロは多くありますが、ここではその中でマクロを活用している処理について2つ紹介します。
まずは指定したstruct list_headのデータから、それを埋め込んだ親構造体を求めるlist_entry()マクロです。
/** * list_entry - get the struct for this entry * @ptr: the &struct list_head pointer. * @type: the type of the struct this is embedded in. * @member: the name of the list_head within the struct. */ #define list_entry(ptr, type, member) \ container_of(ptr, type, member)
これは定義を聞いただけでピンと来るかもしれませんが、内部でcontainer_of()を呼び出しているだけです。これでptr(第一引数)をmember(第三引数)というフィールド名で埋め込んでいるtype型のデータへのポインタを獲得できます。
続いて、リスト内の全エントリを順番に処理するlist_for_each_entry()マクロを見てみます。
/** * list_for_each_entry - iterate over list of given type * @pos: the type * to use as a loop cursor. * @head: the head for your list. * @member: the name of the list_head within the struct. */ #define list_for_each_entry(pos, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member); \ &pos->member != (head); \ pos = list_next_entry(pos, member))
この関数は、headで示されるリストの中の全要素について、各要素をposという名前で取り出すことによってそれぞれに対して処理をします。ここで、posの中でheadに対応するリストはmemberというフィールド名で埋め込まれています。
使用例を示します。サンプルプログラムの仕様とソースは次の通りです。
仕様:
- mylistというリストがある
- mylist内のエントリはint型のnという名前の唯一のデータを持つ
- mylist_show()は、mylist内のすべてのエントリに対してnをカーネルログに出力する
static LIST_HEAD(mylist); struct mylist_entry { struct list_head list; int n; }; ... static void mylist_show(void) { struct mylist_entry *e; printk(KERN_ALERT "mylist: show contents\n"); list_for_each_entry(e, &mylist, list) { printk(KERN_ALERT "\t%d\n", e->n); } }
一見関数のように見える list_for_each_entry()マクロからブロックが生えてその中で処理をしているというのはC言語を知っていれば知っているほど驚くと思いますが、前述のようなマクロ定義をしていればこのような芸当が可能なのです。
linuxカーネルの中には他にも"for_each"という文字列を含む名前の類似のマクロが随所に出てきます。たとえば各エントリの処理中にエントリの削除が可能なlist_for_each_safe()があります。他にも、リストとは別のデータ構造にも類似したAPIが用意されていることもあります。興味があれば、それぞれの定義を見てみると面白いと思います。
おわりに
本記事は執筆時点で自分の脳内にたまたま残っていたマクロについて書いただけなので、他にも面白いマクロはいくらでもあると思います。思い出したらまた追記する予定です。読者のかたがたも、「これも紹介してくれ」とか「このマクロの意味がわからないんだけど」などありましたら、教えていただけると、今後追記するかもしれません。
-
同じ名前で別の引数、戻り値を持つ関数を定義できる機能。例えばswap(int a, int b)とswap(double a, double b)が共存できる。Cでこれをやろうとすると、同名関数が2つ定義されているという旨のコンパイルエラーが出ます。↩
-
最初に条件を満たしたときのみ出力されます。それ故に
_ONCE
という名前が付いています)。↩ -
トークンという言葉がよくわからなければ、ここではなんとなく、コンパイラに変数名や関数名と解釈される文字列と考えてもらっていいです。↩
-
ここでは機能そのものの意味は重要ではないので割愛↩
-
より正確には、これに加えてEXT4_FEATURE_RO_COMPAT_FUNCS()マクロ、およびEXT4_FEATURE_INCOMPAT_FUNCS()マクロも用いる。↩
-
実際には下記定義を直接使うのではなくコンパイラ(通常gcc)組み込みの同等機能を使うのですが、理解を簡単にするためにこちらを例に使います。↩