dump-macros - Cファイル内でdefineされているマクロ一覧を出力する

目的

Cのソースファイル中で、どの #ifdef が有効なのかを調べるために、そのソース内でのdefine済みマクロ一覧を簡単に取得する。

結果

gccのwrapperを作ることで実現できた。

  1. オリジナルコマンドのファイル名をgcc-origに変更する
  2. マクロ表示用gccラッパーをオリジナルコマンド名で置く

コンパイル時間への影響も、10%未満。

背景と動機

Cのソースは、#ifdef 〜 #endif などがとかく混ざりこみやすい(特に、複数プラットフォーム対応などで)。
たいてい、configureを実行して必要なマクロ定義ファイルを作り、その定義内容にしたがってソースの有効箇所を切り替えるつくりになっている。

#ifdefがソース中にあまりに多いと、今のconfigでどの部分が有効なのかがわかりにくくなってくる。
また、マクロ定義内容がconfigによって変わるような場合も、マクロ展開結果が今どうなっているのかの把握が面倒になる。

対策として、まずは今のconfigでdefineされているマクロ一覧を取得することからはじめる。

GCCプリプロセッサ)の機能

GCCプリプロセッサに渡すオプションとして、以下がある:

-E
プリプロッサ起動のみ行う(コンパイラを起動しない)
-dM
通常のプリプロセッサ出力(ソースコード)の代わりに、プリプロセス処理中に定義された全マクロを出力する

これらを使えば、個々のファイルのマクロ定義は取得できる。

しかし、コンパイラ(含むプリプロセッサ)はたいていmakeの中から呼ばれ、そこでは環境変数や引数の設定を色々と行ってくれている。実際のコンパイルと同じ環境を用意してからプリプロセッサを起動しないと、得られるマクロ定義も異なってしまう。

簡単にマクロ定義を取得するために

通常のbuild手順に組み込んでしまうのが、一番簡単だろう。
そのためには、gccのwrapperを作って、

  1. プリプロセッサを起動して、マクロ定義を出力
  2. オリジナルgccを呼び出して、コンパイルをさせる

のように処理すればよさそう。

gcc-wrapper

引数を加工してコマンドを起動するだけなので、shell scriptで十分対応できそうに思うが、受け取った引数を加工してから適切にクオートして別プロセスに渡す、という処理が意外と難しい。これは、shell scriptでは引数が単なるスペース区切りの文字列になってしまうから。

なので、引数を文字列の配列で渡してもらえるように、rubyでwrapperを作成した(単に使い慣れてるからと、Cで書くのは面倒だったから)。

なお、gccで依存関係の自動出力機能を使っている場合(Automakeを使うと勝手にそうなる)には、依存関係出力関連のオプションを取り除かないとgccプリプロセッサ)が何をすればよいか混乱するので、-Mで始まる引数を削除している。。

gcc-wrapper
https://sssvn.jp/svn/spikelet/c/dump-macros/gcc-wrapper
#!/usr/local/bin/ruby
# Dump all macros which defined on compiling to *.m at same place with *.o.
# MEMO: to dump macros, use gcc options -E and -dM.
# MEMO: Don't wrap when compiler reads from stdin,
#       because second-read (original gcc do so) gets empty data.

orig_command = $0 + "-orig"

class Array
  def delete_dependency_args
    each_with_index{ |e,idx|
      if /^-(MF|MT|MQ)$/ =~ e then
        self[idx, 2] = false
      elsif /^-(M|MM|MG|MP|MD|MMD)$/ =~ e
        self[idx] = false
      end
    }
    delete(false)
  end
end

unless ARGV.include? "-"

  opt_c_index = ARGV.index "-c"
  opt_o_index = ARGV.index "-o"

  if opt_c_index and opt_o_index and /\.o$/ =~ ARGV[opt_o_index+1] then

    new_arg = ARGV.clone

    new_arg[opt_o_index+1] = new_arg[opt_o_index+1].sub(/\.o$/, ".m")

    new_arg[opt_c_index,1] = [ "-E", "-dM", "-P" ]
    new_arg.delete "-c"
    new_arg.delete_dependency_args
    
##    STDERR.puts [ orig_command, *new_arg ].inspect
    system(orig_command, *new_arg)

  end
end

exec(orig_command, *ARGV)

処理概要は以下の通り

  1. 標準入力から読み込む場合でなく、かつ -c オプション付きでコンパイルされた場合には、
    1. *.o と同じ場所に *.m というファイル名でマクロ定義を出力する(プリプロセッサ呼び出し)
  2. そしてコンパイル実行(オリジナルgcc呼び出し)

標準入力から読む場合にマクロ出力してしまうと、コンパイラ実行時に標準入力が空になってしまうので、その場合はマクロ出力は諦めた(対策はあるが、glibcコンパイルぐらいにしか見ないケースなのでまあ良いだろう)。

使い方

以下のようにファイルを配置する(gccのパスやgcc-wrapperのコピー元パスは適宜読みかえて):

+ /usr/bin/gcc -> /usr/bin/gcc-orig に名前変更
+ gcc-wrapper -> /usr/bin/gcc として置く

ロスコンパイラの場合は、gccに適宜prefixを付けること。

あとは、通常のbuild手順を踏むことで、*.o と同じディレクトリに、*.mとしてマクロ定義一覧が出力される。

コンパイル時間のオーバーヘッド

通常のビルドに比べて、gcc-wraperとプリプロセッサの起動が追加されるので、確実に遅くなる。
許容できる範囲かどうか、大き目のツリー(事情によりコード規模などは書けない)をビルドして実測してみた。

マクロ出力なし 24:02.52
マクロ出力あり 25:58.53

マクロありにすると、ビルド時間が8%増加した。
この程度なら、ほぼ気にならない、といえる。