関数呼び出しツールを既存のmakefileで使う

前回*1作ったツールは解析対象のソースファイル名を先に持ってきたりとちょっと動かすのが面倒だった。
実際にはソースっていろんなコンパイルオプションがついていたり、インクルードパス設定が複雑だったりする。
ソースを解析するときにそれらを加味して解析ツールを動かすのは大変だねってことで、既存のmakefileを使ったまま解析ツールを動かすために補助ツールを作ってみた。

利用イメージ

まさにkernelソースの解析用に作ったのでnet/socket.cを解析する例を示す。

[root@localhost net]$ pwd
/dev/shm/linux-2.6.39/net
[root@localhost net]# make -C /dev/shm/linux-2.6.39 M=`pwd` CC="translater --pickup-output-filename=/dev/shm/net-socket.csv" socket.o

という感じでCCを上書き指定して補助ツール(translater)を動かすと、補助ツールから前回作った解析ツールをうまく呼び出すようにした。あと、「--pickup-output-filename」引数で出力先ファイル名を指定できるようにした。
kernelは.c単位で.oを作るので、上記のようにmakeのターゲットで.oを指定すればファイル単位で解析が可能。「M=`pwd`」が必須なのかどうかは確認していない。とりあえずつけていても解析はできたのでつけたままにしている。
注意点として、解析処理はファイル単位にしないと本物のコンパイル結果ファイルが生成されないことに起因するエラーでmakeが停止してしまう。このあたりの動作が困る場合は補助ツール側でgccも一緒に起動するように変更してあげれば良いと思う。

補助ツールの仕様

ツールの仕様はこんな感じ。

  1. 前回作ったツールcall_pickerを呼び出す
  2. 「call_picker sourcefile -- clang compile_options」形式で呼び出す
  3. 「--pickup-output-filename=filename」オプションで出力先を標準出力からファイルに切り替えができる

補助ツールとしては解析対象ソース(例えばlinux kernel)のコンパイルオプションを受け取って、call_picker向けにオプションの指定位置を移動させる役割を持たせている。
例えば

gcc -Wall -W -O2 -c -Iinclude/hoge foo.c

みたいなのがmakefileに書いてある場合に、補助ツールにそのまま

translater -Wall -W -O2 -c -Iinclude/hoge foo.c

てな感じでオプションを渡す。(translaterってのが今回作る補助ツール)
そうすると

call_picker foo.c -- clang -Wall -W -O2 -c -Iinclude/hoge

という感じでオプションを振り分けてcall_pickerを呼ぶ。という動作になる。

この補助ツールがあることで既存のmakefileはそのまま無編集でmakeコマンドをたたくときにCCに補助ツールを指定するだけで解析が実行できるようになる、というトリックができるようになる。

make

コンパイルしていたソフトを

make CC="translater --pickup-output-filename=analyze.csv"

みたいにするだけでanalyze.csvファイルに解析結果が出力される、という寸法。

make CC=translater

とシンプルな指定をすることもできて、この場合は標準出力にドバドバ解析結果が表示される。

補助ツールのソース

大したこともやってないし、異常系も何もないつまらないソースだけどそのまま使えるように説明&掲載しておく。

#include <string.h>
#include <stdio.h>
#include <stdlib.h>

static const char *g_src_ext_tbl[] = {
    ".c",
    ".cc",
    ".cxx",
    ".cpp"
};

ソースファイルの拡張子一覧。必要に応じて追加する必要がある。

#define countof(array)  ( sizeof(array) / sizeof(*(array)) )

配列の要素数を返す関数形式マクロ。

static int is_src_ext(const char *filename)
{
    size_t      i;

    for(i=0;i<countof(g_src_ext_tbl);i++){
        if(strcasecmp(filename + strlen(filename) - strlen(g_src_ext_tbl[i]), g_src_ext_tbl[i]) == 0){
            return 1;
        }
    }

    return 0;
}

上記拡張子一覧のテーブル(g_src_ext_tbl)に当てはまるファイル名なら真を返す関数。
strcasecmp()を使っているのでgccコンパイラじゃないとコンパイルできない気がする。
他のコンパイラを使いたい場合は自作するしかないね。
(今回はそもそもclangを利用したツールなのでコンパイラはclang前提になるはずだから問題にならないはず。こだわってネイティブCにそろえる必要はないと判断した)

static int is_file(const char *filename)
{
#ifdef STRICT_LINUX
    struct stat     info;
    stat(filename, &info);
    return S_ISREG(info.st_mode);

#elif defined(STRICT_WINDOWS)

    /* このほかにsymbolic linkをたどる処理が必要... */
    WIN32_FILE_ATTRIBUTE_DATA   info;
    GetFileAttributesEx(filename, GetFileExInfoStandard, &info);
    return (info.dwFileAttributes & (FILE_ATTRIBUTE_DEVICE|FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_INTEGRITY_STREAM)) == 0;

#else
    FILE    *fp = fopen(filename, "rb");

    if(fp != NULL){
        fclose(fp);
        return 1;
    }

    return 0;
#endif
}

ファイルなら真を返す関数。単純にfopen()できるかどうかで判断している。
手元の環境だとディレクトリとかが偽になったので動いている模様。
まじめにやるならifdef側を有効にする必要があるけど、Windows版でシンボリックリンクをたどる方法がわからなかった。Windowsは他にもJunctionとか謎のものもあってよくわからないので手を出すのはやめた。

static void distribute_arg(char **src_bufp, char **arg_bufp, const char *main_arg)
{
    char    **p;

    if(is_file(main_arg) && is_src_ext(main_arg)){
        p = src_bufp;
    }else{
        p = arg_bufp;
    }
    *p += sprintf(*p, " \"%s\"", main_arg);
}

補助ツールのコマンドライン引数(元々makefileコンパイラにわたされていた引数)をソースファイル(src_bufp)かそれ以外(arg_bufp)に振り分ける関数。
ソースファイルかどうかの判断は「ファイルであること」と「拡張子が一致すること」の2つが成立する場合にソースファイルと認識している。

#define OPTION_OUTPUTFILE "--pickup-output-filename="
static const char *parse_args(char *src_buf, char *arg_buf, int argc, char *argv[])
{
    int     i;
    char    *ret = NULL;

    for(i=1;i<argc;i++){
        if(strncmp(argv[i], OPTION_OUTPUTFILE, strlen(OPTION_OUTPUTFILE)) == 0){
            ret = &argv[i][strlen(OPTION_OUTPUTFILE)];
        }else{
            distribute_arg(&src_buf, &arg_buf, argv[i]);
        }
    }

    return ret;
}

コマンドライン引数を解釈する関数。出力ファイルの指定(--pickup-output-filename)かどうかで条件判定して、出力ファイルの指定ならファイル名部分を戻り値に設定する。他のオプションだったら振り分け関数へ渡してソースファイルとそれ以外に分けてもらう。

static void invoke_picker(char *buf, const char *srcs, const char *args, const char *output_file)
{
    if(output_file == NULL){
        sprintf(buf, "call_picker %s -- clang %s",       srcs, args);
    }else{
        sprintf(buf, "call_picker %s -- clang %s >> %s", srcs, args, output_file);
    }

    system(buf);
}

call_picker呼び出し用の文字列を作ってsystem()関数で実行する関数。分岐は出力ファイル名のあり/無し。ありの場合は単にリダイレクトしているだけ。

#define BUF_SIZE    4096
static char g_src_buf[BUF_SIZE];
static char g_arg_buf[BUF_SIZE];
static char g_cmd_buf[BUF_SIZE];

int main(int argc, char *argv[])
{
    const char *output_file;

    output_file = parse_args(g_src_buf, g_arg_buf, argc, argv);
    invoke_picker(g_cmd_buf, g_src_buf, g_arg_buf, output_file);

    return 0;
}

main()関数と作業用のバッファ。ちなみにバッファサイズは1024だとlinux kernelのコンパイルオプションを覚えておくには足りなかった。オプションの長さチェックをやっていないのでBUF_SIZEはあらかじめ十分大きな値としておく必要がある。(バッファオーバーフローしちゃうね。今回はつまらない単機能ツールだから特にその辺りは手抜きで十分との認識)
main()は引数を解釈する関数と実際にコマンド実行する関数を呼ぶだけのシンプルなつくり。

補助ツールのコンパイル

Windowsだとこんな感じ。特にオプション類は不要。最適化オプションとかWarningオプションとかを好きにつけてもOK。Linuxだと「.exe」は不要。
translater.cは上記の細切れソース断片を全部1ファイルに張り付けたものでOK。

clang -o translater.exe translater.c

さて

これでkernelをはじめとしたmakefileでビルドするソフトのソースコードについて、関数呼び出しをCSVに吐き出せるようになったわけだが。これを利用した別のツールを今作っている。プロトタイプはできたが、もう少し機能を追加してから公開しようと思う。
(いつになるのやら…)