第1回と第2回ではKiloのソースコードをざっと読んでその処理の流れを把握した。Kiloは基本的に、キーボードから1バイト入力があるまで待って、1バイト何か入力されるとその入力された文字やキーに合わせて処理をして、画面を全部更新する(1画面分出力する)、といった処理を繰り返していることを説明した。今回はソースコードを読む上で必須になってくるマクロを取り上げる。
Cのソースコードには、次のように、大文字で記述された変数のようなものが書いてあることがある。kilo.cなら次のような行が見つかる。STDIN_FILENOという部分に注目だ。
標準で定義されているマクロが使われているサンプル
enableRawMode(STDIN_FILENO);
C言語の授業や実習を受けたことがあるなら記憶にあるかもしれないが、これは標準出力のファイルディスクリプタを記述したものだ。実際には、unistd.hというファイルで定義されたもので、次のように記載されている。
STDIN_FILENOはunistd.hで定義されている
#define STDIN_FILENO 0
これはマクロと呼ばれる機能で、コンパイル時に最初に置き換えられる。つまり、コンパイルする時はSTDIN_FILENOと書いてある部分は全部0に置き換わってから、コンパイルが行われることになる。C言語のソースコードではこのマクロで定義された何かをいたるところで見ることになる。
kilo.cであれば、例えばeditorSyntaxToColor()という関数を読むと次のようになっている。
int editorSyntaxToColor(int hl) {
switch(hl) {
case HL_COMMENT:
case HL_MLCOMMENT: return 36; /* cyan */
case HL_KEYWORD1: return 33; /* yellow */
case HL_KEYWORD2: return 32; /* green */
case HL_STRING: return 35; /* magenta */
case HL_NUMBER: return 31; /* red */
case HL_MATCH: return 34; /* blu */
default: return 37; /* white */
}
}
switch構文で処理を切り分けているわけだが、そこでHL_COMMENT、HL_MLCOMMENT、HL_KEYWORD1、HL_KEYWORD2、HL_STRING、HL_NUMBER、HL_MATCHという言葉が使われている。これは学校では習わなかったはずだ。これらはkilo.cの55行目辺りから定義されているマクロだ。次のように定義されている。
editorSyntaxToColor()関数のswitch構文で使われているマクロの定義部分
/* Syntax highlight types */
#define HL_NORMAL 0
#define HL_NONPRINT 1
#define HL_COMMENT 2 /* Single line comment. */
#define HL_MLCOMMENT 3 /* Multi-line comment. */
#define HL_KEYWORD1 4
#define HL_KEYWORD2 5
#define HL_STRING 6
#define HL_NUMBER 7
#define HL_MATCH 8 /* Search match. */
要するに、HL_NORMAL、HL_NONPRINT、HL_COMMENT、HL_MLCOMMENT、HL_KEYWORD1、HL_KEYWORD2、HL_STRING、HL_NUMBER、HL_MATCHは0、1、2、3、4、5、6、7、8という数字に置き換わるわけだ。
こんな感じで、C言語ではファイルの先頭のほうやヘッダファイルでマクロが定義されており、慣例的にマクロはすべて大文字で記述されていることが多い。
ちなみに、最初は「なぜそのように定義されているのか」とか、「どうしてこれがマクロで記述されているのか」とかは、あまり考えないほうがいい(開発者にも癖や好みがあって、特に意味がなくてもマクロになっていることがあるので)。初めは、「これはマクロであり、どこかで定義された別の何かなんだな」ということがわかっていればよいと思う。
マクロと似たような用途で、列挙型が使われることもある。前回も取り上げたeditorProcessKeypress()関数だが、この関数の処理の本質部分だけ抜き出すと、次のような感じになっている。入力された1バイトのデータに合わせて処理を振り分けるというものだ。
editorProcessKeypress()関数の処理の本質的な部分(抜粋&編集)
void editorProcessKeypress(int fd) {
int c = editorReadKey(fd);
switch(c) {
case ENTER:
case CTRL_C:
case CTRL_Q:
case CTRL_S:
case CTRL_F:
case BACKSPACE:
case CTRL_H:
case DEL_KEY:
case PAGE_UP:
case PAGE_DOWN:
case ARROW_UP:
case ARROW_DOWN:
case ARROW_LEFT:
case ARROW_RIGHT:
case CTRL_L:
case ESC:
default:
}
quit_times = KILO_QUIT_TIMES;
}
この関数のswtich構文で使われているENTER、CTRL_C、CTRL_Q、CTRL_S、CTRL_F、BACKSPACE、CTRL_H、DEL_KEY、PAGE_UP、PAGE_DOWN、ARROW_UP、ARROW_DOWN、ARROW_LEFT、ARROW_RIGHT、CTRL_L、ESCはマクロのようだが、これらは列挙型で定義されている。kilo.cの110行目辺りから定義されているKEY_ACTIONという列挙型がそれだ。定義部分は次のようになっている。
列挙型として定義されたKEY\_ACTION
enum KEY_ACTION{
KEY_NULL = 0, /* NULL */
CTRL_C = 3, /* Ctrl-c */
CTRL_D = 4, /* Ctrl-d */
CTRL_F = 6, /* Ctrl-f */
CTRL_H = 8, /* Ctrl-h */
TAB = 9, /* Tab */
CTRL_L = 12, /* Ctrl+l */
ENTER = 13, /* Enter */
CTRL_Q = 17, /* Ctrl-q */
CTRL_S = 19, /* Ctrl-s */
CTRL_U = 21, /* Ctrl-u */
ESC = 27, /* Escape */
BACKSPACE = 127, /* Backspace */
/* The following are just soft codes, not really reported by the
* terminal directly. */
ARROW_LEFT = 1000,
ARROW_RIGHT,
ARROW_UP,
ARROW_DOWN,
DEL_KEY,
HOME_KEY,
END_KEY,
PAGE_UP,
PAGE_DOWN
};
マクロと列挙型は別のものだ。列挙型は型チェックができるし、コンパイル時の挙動がマクロとは異なる。きっちり検討していけば、このケースではマクロ、このケースでは列挙型を使うべきだろう、といった議論はできる。
だが、実際のところ、マクロを使うか列挙型を使うかは、そのソースコードを記述する開発者の癖や好みによるところが大きい。同じ人物でも気分やタイミングで変えるかもしれない(例えば、筆者はどちらかと言えば列挙型よりもマクロを使うことが多い)。
したがって、マクロであるか列挙型であるかについても、あまり深く考えないことだ。kilo.cのソースコードのように、「swtich構文で分岐する時に使う何かだ」のように、最初は理解しておいてもよいと思う。
なお、マクロはただの文字列の置き換えのようなものなので、次のような使い方をすることもある。これもkilo.cの中にある記述だ。
/* Highlight */
FIND_RESTORE_HL;
学校の授業や演習でC言語を勉強しただけなら、この記述は謎にか思えないだろう。これは処理自体をマクロで定義したもので、実際には次のように定義されている。
FIND_RESTORE_HLを定義しているコード
#define FIND_RESTORE_HL do { \
if (saved_hl) { \
memcpy(E.row[saved_hl_line].hl,saved_hl, E.row[saved_hl_line].rsize); \
saved_hl = NULL; \
} \
} while (0)
FIND_RESTORE_HLは「do { if (saved_hl) { memcpy(E.row[saved_hl_line].hl,saved_hl, E.row[saved_hl_line].rsize); saved_hl = NULL; } } while (0)」という処理に置き換わることになる。「do { ...処理... } while (0)」という記述になっているのは、マクロの書き方上の理由があったりするのだが、今はこう書くものだと思っておいてもらえればよいだろう。
処理をマクロにまとめるのにも理由がある。「デュプレックスコードを避けたいが関数にするのは嫌だ」とか、「関数コールになるのは遅くなりそうだから避けたい」とかといった具合だ。
逆に、読みにくくなるのでこうした書き方を嫌う開発者もいる(自分が書く分にはよいのだが、他の開発者が書いたマクロベースのコードは読みたくないとかもある)。
マクロ定義はネストしていることもあって、マクロの定義部分でさらにマクロを使って、そのマクロの定義場所でまた別のマクロを使って、といったように、マクロの深遠に落ちていくこともある。そうした場合は、とりあえず全部追っていくのではなく、とりあえず「こういうものだろう」と仮定しておいて別の部分を読みにいってしまおう。ソースコード読みで1カ所ではまり続けるとそれ以上進まなくなってしまう。
マクロはファイルの先頭の方にまとめて定義されることが多い。または、ヘッダファイルでまとめて定義される。このマクロは意味のある何かを整理した結果になっていることが多く、定義されているマクロを眺めていると、「そのソースコードがどのようなことをするものなのか」「どのようなことに使われるのか」「どのようなことに使われることを想定しているのか」が見えてくる。C言語に慣れた開発者なら、マクロの部分を見るとぼんやりとソースコードの中身が予測できるものだ。
kilo.cのコードもマクロの定義部分を見ると、大体どんなことを考えて作られているのかが見えてくる。マクロは実際のコードでも多用されている重要な機能だが、学校の授業や演習をやっただけだと、今ひとつ存在意義が実感できないように思う。
kilo.cで使われているマクロは典型的でわかりやすいので、ぜひこのコードでマクロがどのような用途で使われるのかを感じてもらえればと思う。