cursesなどのライブラリも使わずに、C言語1000行ほどで開発されたエディタ「Kilo」のソースコードを読む本連載。前回、main()から流れを追って、Kiloの処理の内部構造がおおむね以下のような感じになっていることを説明した。

  1. ターミナルに画面分文字を出力
  2. キー入力を待つ
  3. 入力されたキーに応じて処理を実施
  4. 1.に戻る

2回目となる今回は、実際に関数を追いながら、キー入力を待っている部分まで処理をたどってみよう。まず、main()関数の該当部分だけを抜粋したものを以下に示そう。

int main() {

while(1) {
    editorRefreshScreen();
    editorProcessKeypress(STDIN_FILENO);
}
}

処理としてはmain()関数の内部のeditorProcessKeypress()の部分でキー入力を待っているんだろうな、ということがここで予測できる。なぜかと言うと、ほかにその処理が見当たらず、関数の名前もそれっぽいからだ。

コードを読む際、「たぶんこの中だ」といった決めつけをしながら、実際にほかの部分のソースコードを読んで理解を修正して、といった作業を繰り返して中身を理解していくとよい。

このeditorProcessKeypress()関数の中身について、主な処理のフローだけに注目してザックリ抜き出すと次のようになる。editorReadKey()でキー入力を待ち、以降は入力されたキーに応じて処理を振り分ける内容になっている(コメントや実際の処理は抜いているので、詳しく知りたい場合は元のソースコード(https://github.com/antirez/kilo/blob/master/kilo.c)を参照のこと)。

void editorProcessKeypress(int fd) {

int c = editorReadKey(fd);

switch(c) {
case ENTER:
    break;
case CTRL_C:
    break;
case CTRL_Q:
    break;
case CTRL_S:
    break;
case CTRL_F:
    break;
case BACKSPACE:
case CTRL_H:
case DEL_KEY:
    break;
case PAGE_UP:
case PAGE_DOWN:
    break;
case ARROW_UP:
case ARROW_DOWN:
case ARROW_LEFT:
case ARROW_RIGHT:
    break;
case CTRL_L:
    break;
case ESC:
    break;
default:
    break;
}
}

editorProcessKeypress()で呼ばれているeditorReadKey()を追って、こちらも処理の本質部分だけをザックリ抜き出すと次のようになる。「while ((nread = read(fd,&c,1)) == 0);」というところがポイントだ。「read(fd,&c,1)」という処理で、標準入力から1バイトだけ入力を受け付けている。つまり、入力が行われるまでここで待ち処理を発生させている。キーを押すと「read(fd,&c,1)」の処理が実行されて、cに入力した文字が入るという寸法だ。

int editorReadKey(int fd) {
int nread;
char c, seq[3];

// ここで1バイト読み込むまで待機
while ((nread = read(fd,&c,1)) == 0);

while(1) {
    switch(c) {
    case ESC:
        break;
    default:
        return c;
    }
}
}

read()以降は、入力されたデータがESCキーだった場合はESCシーケンスとして処理するために処理を行い、それ以外の場合は入力された文字をそのままeditorProcessKeypress()に戻している。editorProcessKeypress()では文字に応じて処理を行っている。そして、main()まで戻り、main()からeditorRefreshScreen()が呼ばれてまた画面が書き換えられる、という仕組みになっている。

プログラミング的ロジック発想に慣れていない方は、そろそろさじを投げ出す頃だと思うが、もうちょっと辛抱いただきたい。まだいけるはずだ。ざっくり流れをまとめると次のようになる。

  1. main()
  2. main: editorProcessKeypress()
  3. main: editorProcessKeypress: editorReadKey()
  4. main: editorProcessKeypress: editorReadKey: read() 1バイト読むまで待つ
  5. main: editorProcessKeypress: editorReadKey: ESCだったらESCシーケンスを処理
  6. main: editorProcessKeypress: editorReadKey: 読み込んだ文字を返す
  7. main: editorProcessKeypress: 返ってきた文字に合わせて処理
  8. main: editorRefreshScreen()
  9. main: editorRefreshScreen() 1画面出力
  10. main: 2.に戻る

大丈夫、大丈夫、まだページは閉じないでほしい! もっと簡単に考えるとこうなる。

  1. Kilo主処理→キー処理→1文字入力待ち
  2. Kilo主処理→キー処理
  3. Kilo主処理→画面を更新
  4. Kilo主処理: 1.に戻る

C言語に限った話ではないが、ソースコードを最初から最後まで子細に全部把握し理解しようとすることは、プログラミングに慣れている場合を除いてやめたほうがいい。大抵は途中で投げ出したくなる。

そこで、わからない部分はとりあえず放っておき、最も基本となる処理の流れ、フローに注目して流れを追ってみよう。すると、ふわっと全体が見えてくるはずだ。そうやって何度かフローを追っていくと、今までぼんやり見えていた木の幹がはっきりしてきて、そこから出ている枝や、先っちょの葉っぱまで細かくクリアに見えてくる。最初から、細かな部分を見ようとしないほうがよいと思う。

逆に、ある程度プログラミングに慣れているなら、kilo.cの最初のほうに書いてある構造体や列挙型、マクロなどを読むと内部構造とフローの想像がつくはずだ(この辺りは後々説明するので、今わからなくても大丈夫だ)。データ構造から中身を推測してソースコードを読み、読みながら自分の予想と実際の実装の違いを擦り合わせて修正し、頭の中にソースコードの地図を描いていく。

今回はこの辺りにしておこう。ざっと読みつつ、わからない部分はそのまま放置しておく。枝葉末節にとらわれず、とりあえず「ここはよくわからないが、たぶん〇〇をしているような気がする」といった程度に理解しておけばよい。kilo.cはそれほど関数の呼び出しが深くないので、落ち着いて何度か読んでみれば本質的なフローが見えてくるはずだ。