ソースコードから標準Cライブラリに含まれてそうな識別子を検索する

組み込み関連でクロスプラットホームなライブラリを開発する時、私の周囲十数メートルの範囲では、使用している標準Cライブラリの機能をドキュメントに明記することがある。というのも、そのライブラリがフリースタンディング環境で使用される可能性があるからだ。

フリースタンディング環境では、使用可能な標準ライブラリの機能が限定される。極端な話、ANSI(C89、C90)規格上はstring.hが提供する機能すらもサポートされていない。開発環境によっては標準ライブラリの一部機能(時には全機能)が提供されていることも多いが、それが望めない場合は自分で実装することになる。*1

このような事情がある為、ライブラリを開発する側は使用している標準ライブラリの機能を明記しておき、ライブラリを使用する側は各自の開発環境にてそれらの機能がサポートされているかチェックしている。

さて本題。ライブラリを開発する側としては、この「使用している標準ライブラリ機能を明記する」という作業は中々面倒だ。手作業では時間が掛かるし見落としも発生する。ライブラリの規模が大きくなれば、とても手作業ではやってられない。

「開発中に使用する機能を小まめにメモしていく」という方法も考えられるが、ライブラリの修正や機能追加に完全に対応しきれるものではない。例えば、ソースコードの書き換えや削除が何度か発生したCやC++ソースコードでは、不要なヘッダがインクルードされていることが多い。おそらく、以前のリリースではそのヘッダで公開されている関数やクラスを使用していたのだが、版を重ねてソースコードが変化していくうちに削除されていったのだろう。これと同様の現象が、使用している標準ライブラリ機能のメモでも起こりうる。

そんな訳で「せめて標準Cライブラリで使われていそうな識別子を自動的にgrepしたいなあ」という要求が生まれてくる。

この作業をより厳密に行うのならば、C言語の文法に基づいてソースファイルの内容をパースして、識別子に対してマッチングを行う必要がある。が、しかし、今回はそこまでは考えない(むしろ自分の力量的にそこまで考えられない……)。

幸いにも手元にotbedit用のC++のキーワード設定ファイルを大幅に改定したものがあって、C89の標準Cライブラリのマクロ名や関数名はほぼ全て揃っている。そこでこのデータを使ってちょっとしたツールを作ることにした。

まずプロトタイプを作ってみた。

#!/bin/sh

while read line; do
    grep -E -n "\<$line\>" ${@+"$@"}
done <<'EOP'
BUFSIZ
CLOCKS_PER_SEC
EDOM
EOF
ERANGE
EXIT_FAILURE
EXIT_SUCCESS
FILE
FILENAME_MAX
FOPEN_MAX
HUGE_VAL
EOP

ヒアドキュメントで識別子を抱えている。本来はもっと大量にあるのだけど邪魔なので省略している。

この実装は動くには動くが、色々と問題がある。出力結果はgrepしたファイルごとに分かれていないし、行番号ごとにソートされている訳でもない。重複もあるだろう。

出力結果から重複を削って且つ見やすくソートするとこうなる。

#!/bin/sh

while read line; do
    grep -E -H -n "\<$line\>" ${@+"$@"}
done <<'EOP' | sort -k 2 -n -t ':' | sort -k 1,1 -s -t ':' | uniq
BUFSIZ
CLOCKS_PER_SEC
EDOM
EOF
ERANGE
EXIT_FAILURE
EXIT_SUCCESS
FILE
FILENAME_MAX
FOPEN_MAX
HUGE_VAL
EOP

ところでこのツールを実際に使ってみると結構重い。何も考えずにC89の標準ライブラリ絡みの識別子を抜き出すと250個ぐらいある。つまり250回もgrep(1)を実行しているのだ。grep(1)を実行する度にプロセスの生成と各ファイルの読み出しが発生する。重くなるはずだ。

この問題を避けるため、grep(1)のオプション -f を使用して全パターン読み込ませるようにする。オプション -w を使うと単語単位でマッチングできるので、各パターンを \< と \> で囲む必要もない。あと、最近の事情は分からないが、20年ぐらい前の情報によれば、大量のキーワードを検索するならfgrep(1)(つまりgrep -F)のアルゴリズムの方が有利らしい。

#!/bin/sh

grep -F -f - -H -n -w ${@+"$@"} <<'EOP'
BUFSIZ
CLOCKS_PER_SEC
EDOM
EOF
ERANGE
EXIT_FAILURE
EXIT_SUCCESS
FILE
FILENAME_MAX
FOPEN_MAX
HUGE_VAL
EOP

これで実行回数が1回になり高速化された上に、重複やら何やらに対応しようと頑張っていた処理も不要になった。

教訓:ちゃんとman(1)を読みましょう。

*1:もっとも私の場合、標準ライブラリの機能がある程度サポートされた環境での開発しか経験していない。