make-filter-out - filter-out関数の使い方

目的

GNU make の関数 $(filter-out) の使い方をメモしておく。

結果

filter-outを使うと、変数内の特定の単語を、完全一致/前方一致/後方一致で取り除くことができる。
以下、詳細

filter-outとは

GNU Makeのmanual(GNU make: Text Functions)を参照すればまあ書いてあるのだが、textからwordにマッチしない要素を取り出す関数。text, wordともにスペース区切りで複数要素を指定可能。

たとえばMakefile中のCFLAGS指定などで、

CFLAGS = -O2 -c -fomit-frame-pointer -fstrength-reduce

などと指定されている時に、CFLAGSは流用したいけど、最適化はオフにしたい(-O2オプションはいらない)、という場合に使用する。

なにができるのか、試してみる

filter-outの実力を試すテストMakefileを作成した。

filter-out.mk
https://sssvn.jp/svn/spikelet/make/filter-out.mk
CFLAGS = -O -O2 -c -O1 -fomit-frame-pointer -fstrength-reduce -Os
test:
        @echo ORIGINAL:  $(CFLAGS)
        @echo 'filter-out with "-O"      :' $(filter-out -O, $(CFLAGS))
        @echo 'filter-out with "-O1"     :' $(filter-out -O1, $(CFLAGS))
        @echo 'filter-out with "-O2"     :' $(filter-out -O2, $(CFLAGS))
        @echo 'filter-out with "-O%"     :' $(filter-out -O%, $(CFLAGS))
        @echo 'filter-out with "%s"      :' $(filter-out %s, $(CFLAGS))
        @echo 'filter-out with "-f%r"    :' $(filter-out -f%r, $(CFLAGS))
        @echo 'filter-out with "%s%"     :' $(filter-out %s%, $(CFLAGS))
        @echo 'filter-out with "-f% -O%" :' $(filter-out -f% -O%, $(CFLAGS))

実行結果は以下:

% make -f filter-out.mk
ORIGINAL: -O -O2 -c -O1 -fomit-frame-pointer -fstrength-reduce -Os
filter-out with "-O"      : -O2 -c -O1 -fomit-frame-pointer -fstrength-reduce -Os
filter-out with "-O1"     : -O -O2 -c -fomit-frame-pointer -fstrength-reduce -Os
filter-out with "-O2"     : -O -c -O1 -fomit-frame-pointer -fstrength-reduce -Os
filter-out with "-O%"     : -c -fomit-frame-pointer -fstrength-reduce
filter-out with "%s"      : -O -O2 -c -O1 -fomit-frame-pointer -fstrength-reduce
filter-out with "-f%r"    : -O -O2 -c -O1 -fstrength-reduce -Os
filter-out with "%s%"     : -O -O2 -c -O1 -fomit-frame-pointer -fstrength-reduce -Os
filter-out with "-f% -O%" : -c

考察:

  1. wordは完全一致させないといけない("-O"では"-O1"などが取り除かれない)
  2. "%"を使えば、前方/後方/前方と後方一致もできる ("-O%"で"-O1", "-O2", "-Os"がすべて取り除かれている)
  3. "%"は空文字列にもマッチする ("-O%" で "-O" も取り除かれる)
  4. "%"を2回以上使うことはできない ("%s" ではなにも取り除かれない)
  5. 削除対象のwordが複数個でも "%" は普通に使える

make のソースを見てみる

filter-out が実際になにをしているのか、興味本位で解析してみる。
また、Make中での関数の定義方法、呼び出し方もついでの抑えておく。

対象バージョン

GNU make 3.80(RHEL4のmakeのバージョン)。

わかったことの概要
  1. Makeのビルトイン関数処理
    1. 各関数は、function.c の function_table_init[] に定義されている(ここ以外にはない)
    2. 処理関数(のポインタ)は function_table_init の最後のメンバーで、全て function.c に書かれている
    3. 関数の引数が少ない場合はエラーになるが、多い場合は後ろの方を1つの引数として処理する
    4. Make内部では、ハッシュを使って関数名でのエントリ検索を高速化している
    5. ビルトイン関数は、実は $(call..) を使っても呼ぶことができる
    6. 関数名に '_' は使えない(英小文字とハイフンだけ)
    7. 処理関数の引数は、(1)展開結果バッファのWrite Pointer、(2)引数の配列、(3)呼び出された関数名、を受け取る
    8. 変数名の展開結果などは、variable_buffer_output() を読んで書き出す
    9. 処理関数の返り値は、展開結果バッファのWrite Pointerを返す(展開結果などを返すわけではない)

処理中のMakefileのポインタ

  1. filter-out 関数
    1. filter と filter-out の実体は同じ関数で、名前で処理を分けている
    2. word, pattern それぞれ要素が複数あっても処理する
    3. パターンマッチの '%' は、pattern の中のどこに(途中にでも)合っても良いが、1つしかきちんと処理されない
    4. filter-out内でも、検索高速化のためにハッシュテーブルを使う場合がある(要素数が多い場合など)
  2. その他
    1. 関数宣言が、3.80 までは K&R スタイル、3.81 では ANSI スタイル
    2. ただし、K&R対応のコンパイラでもGNU Makeがビルドできるように、ソースの変換ツールを同梱
Makeの関数定義の場所

Makeの関数はソース中の function.c の function_table_init 配列に置いてある。

struct function_table_entry
  {
    const char *name;                /* 関数名 */
    unsigned char len;               /* 関数名の長さ(hash中での比較の高速化のため) */
    unsigned char minimum_args;      /* 引数の個数の最小値 */
    unsigned char maximum_args;      /* 引数の個数の最大値(0ならば無制限) */
    char expand_args;                /* 実行前に全引数を展開するか?(foreachなどは展開されると都合が悪いので0にする) */
    char *(*func_ptr) PARAMS ((char *output, char **argv, const char *fname));  /* 関数本体 */
  };
...
static struct function_table_entry function_table_init[] =
{
 /* Name/size */                    /* MIN MAX EXP? Function */
...
  { STRING_SIZE_TUPLE("filter-out"),    2,  2,  1,  func_filter_filterout},
関数のハッシュテーブル

Make内では関数の検索がしやすいように、配列からハッシュテーブルを作成している。

作成箇所: function.c 内の hash_init_function_table() 内で

static struct hash_table function_table;
....
void
hash_init_function_table ()
{
  hash_init (&function_table, FUNCTION_TABLE_ENTRIES * 2,
	     function_table_entry_hash_1, function_table_entry_hash_2,
	     function_table_entry_hash_cmp);
  hash_load (&function_table, function_table_init,
	     FUNCTION_TABLE_ENTRIES, sizeof (struct function_table_entry));
}

hash関連の関数は hash.c 内にある。
hash_init()でMakeの関数テーブルを初期化する。function_table は 2のべき乗のサイズを持つハッシュ。function_table_entry_hash1/2がhash関数で、hash2は強制的に奇数の値を算出する(ハッシュテーブルを効率良く使うため)。function_table_entry_hash_cmpは、ハッシュのキーを文字列として比較する関数。
hash_load()で、function_table_initの内容をhashに入れる。
lookup_function() で文字列からハッシュを検索して、該当する function_table_entry を返す(見つからなければ0を返す)。
関数名は、正規表現で書くと、/^[a-z-]+[\0\t ]/ にマッチすること。

関数の呼び出し

Make関数の呼び出しは、二通りの方法がある(ことを今知った)。
一つは、$(func ...)形式、もう一つはユーザ定義関数を呼び出す call 関数内でMakeのビルトイン関数を指定した場合。
どちらも最終は expand_builtin_function() に飛ぶので、$(func)形式の時の処理だけを示す:

  1. variable_expand_string() in expand.c
    1. '$'の次が '('か'{'なら、とりあえず関数か試すために handle_function()を呼ぶ (返り値が非0なら=関数だったら、展開完了)
  2. handle_function() in function.c
    1. 関数名をハッシュテーブルで検索して、無ければ return 0
    2. 引数の解析して、対応する閉じカッコ(')' か '}')が無ければ return 0
    3. この時、maximum_args より引数が多いときは、セパレータのコンマ以降も一つの引数とみなしてくっつける(『引数が多過ぎる』エラーは出さない)
    4. expand_args が非0なら、引数を展開する(展開は関数・変数どちらも行うので、入れ子の関数もここで処理する)
    5. expand_builtin_function() 呼出し (関数展開が成功しても失敗しても、handle_function は return 1)
  3. expand_builtin_function() in function.c
    1. minimum_argsのチェック、少なすぎたらfatal()で異常終了
    2. func_ptr(Make関数の実体)がNULLなら (unimplementedなら) fatal()で異常終了
    3. Make関数の実体(関数ポインタ)を呼ぶ
filter-outの場合

func_ptrは func_filter_filterout()。
これは、filterとfilter-outで同じ関数で処理している。

  1. func_filter_filterout() in function.c の処理
    1. pattern(第一引数)の各要素取り出し、'%'があるかもチェック(最初にある'%'のみチェック。%はバックスラッシュでquoteできる)
    2. word(第二引数)の各要素取り出し
    3. 検索にhashを使った方が効率的と判断したら、wordのハッシュテーブルを作る(patternに'%'無しのリテラルが2個以上、など)
    4. 各patternごとに、wordの各要素と比較 ('%'を含む場合は pattern_matches()で比較するが、やはり '%' は1つしか想定していない)
    5. 呼びだし関数名が "filter" ならマッチしたwordを、そうでなければマッチしなかったwordを展開結果として出力 (variable_buffer_output()での出力)
    6. 関数の返り値は、出力バッファのWrite Pointer
他に気付いたこと: GNU Makeのコーディングスタイル

3.80 は K&R スタイル(引数の型は関数定義のカッコの外に書く)。ただし、一部 void 型などは使っている。
3.81 は ANSI スタイル(引数の型と名前両方をカッコの中に書く)。

おそらく、GNU Make 自体のポータビリティを高めるために K&R スタイルで書いていたんだろうなあ。
3.81 では、ansi2knr.c という 関数宣言のANSIスタイル→K&Rスタイル変換ツールが入っていて、configure時にコンパイラK&Rしか対応していないかをチェックし、その場合にはansi2knrを通してソースを整形してからコンパイルする、という処理が入っている。ご苦労さまです。