Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Plan 9のCプリプロセッサを読んだ

Plan 9には cpp(1) というANSIに準拠したCプリプロセッサが用意されていますが、Plan 9標準のCコンパイラコンパイラ自身が簡略化したプリプロセッサを持っているので基本的には使いません。ただし、Cコンパイラが持っているプリプロセッサは設計思想の影響もあり大幅に簡略化されているので、ANSI準拠のプリプロセッサが必要な場合は cpp を使います。8c(1)の場合は -p オプションが渡されれば cpp が使われるようになりますし、APE(ANSI/POSIX Environment)用のCコンパイラpcc(1)は特に何もしなくても cpp が使われます。*1

この cpp#include_next というGCC拡張ディレクティブを追加したくて関連するコードを読みました。

必要な理由

なんで #include_next を追加する必要があったのかというと、いくつかUnix由来のソースコードPlan 9へ移植していた時に、#include_nextが使われているものがありました。この拡張はシステム標準のヘッダファイルから一部を書き換えたい場合に使うことを想定しています。例えば uint32_t 型がシステムによって提供されていない場合、

#include_next <stdint.h>

typedef ulong uint32_t;

という内容を stdint.h というファイル名で #include のサーチパスに入れておくと、ソースコードからは単純に#include <stdint.h>とすればuint32_tが使える状態で読み込まれるという便利なものです。

Plan 9cpp は、実行されない場所に書かれたディレクティブであっても解析はするので、

#if 0
#include_next <stdint.h>
#endif

上記のコードでも #include_next がパースされて、結果として不明なディレクティブなのでエラーになってしまいます。このエラーを簡単に避ける方法がありませんでした。

データ構造

#include のサーチパスは includelist[] によって表現されます。

typedef struct  includelist {
    char    deleted; // 1なら参照しない
    char    always;  // 0の場合は "file.h" のみ対象
    char    *file;   // ディレクトリ名(例: /sys/include)
} Includelist;

#define    NINCLUDE 64
Includelist includelist[NINCLUDE];

デフォルトでは /$objtype/include/sys/include がサーチパスに入っています。また、$include 環境変数がセットされていれば、その内容も含まれます。特にオプションを渡さない場合、以下のような配列になります。

[0] file=/$objtype/include always=1
[1] file=/sys/include always=1
[2] file=$include(1) always=1
[3] file=$include(2) always=1
...
[63] file=. always=0

cpp-I オプションを渡した場合、includelist の末尾に追加されていきます。例えば cpp -I/a/include -I/b/include の場合は以下のようになります。

[0] file=/$objtype/include always=1
[1] file=/sys/include always=1
[2] file=$include(1) always=1
[3] file=$include(2) always=1
...
[61] file=/b/include always=1
[62] file=/a/include always=1
[63] file=. always=0

もう一つ、Source *cursource も重要なデータで、現在処理中のファイルのスタックを表します。

typedef struct source {
    char    *filename;  /* name of file of the source */
    int line;       /* current line number */
    int lineinc;    /* adjustment for \\n lines */
    uchar   *inb;       /* input buffer */
    uchar   *inp;       /* input pointer */
    uchar   *inl;       /* end of input */
    int     ins;        /* input buffer size */
    int fd;     /* input source */
    int ifdepth;    /* conditional nesting in include */
    struct  source *next;   /* stack for #include */
} Source;

Source  *cursource;

これはリンクリストになっていて、現在処理中のファイルが先頭です。

検索

検索する場合は、例えば #include <stdio.h> なら、includelist を後ろから検索していきます*2。この時、deletedが1の場合は常に無視し、alwaysが0の場合は #include <xx> の対象となりません(#include "xx" なら対象)。そうして、stdio.h が見つかったら探索を止めて cursource を更新します*3

#includeがネストした場合は、もう一度 includelist を後ろから検索してファイルを探します。見つかったら cursource のリストが増えて、処理し終われば取り除かれて cursource が以前処理していたファイルに戻ります。

*1:pcc はデフォルトのオプションやサーチパスが異なるだけで、コンパイル自体は 8c で行います

*2:この辺りのコードは /sys/src/cmd/cpp/include.c に書かれています

*3:これは /sys/src/cmd/cpp/lex.csetsourceunsetsource が行います