2012年7月19日木曜日

Eclipse CDT C言語パーサーに好きなヘッダファイルを使わせる

今まで Eclipse CDT でC言語のパースをする場合

IncludeFileContentProvider
にはIncludeFileContentProvider.getEmptyFilesProvider()を指定していました。(CDTでASTを参照)

ところが今日、この方法を使うとある問題にぶち当たることがわかりました。
想像はしていましたが、ヘッダファイルがまったく #include されずにパースが行われるのです。それでもなんとなくパースできちゃうのがCDTのすごいところですが、問題は識別子の型を正確に取れないということです。
ソースコードに現れる名前、例えば「add」が変数名なのか何なのかは、その宣言が無いと分かりません。たとえ「add(2, 3)」のように、いかにも関数呼び出し風に書かれていたとしても、もしかしたら関数ポインタ、すなわち単なる変数かもしれないのです。

ということで、正確に識別子の型を判別するには #include を無視せずにヘッダファイルを読み込むようにしなければなりません。そこで登場するのが
IncludeFileContentProvider.getSavedFilesProvider()
です。 SavedFilesProvider は、パース対象のC言語ソースコードがある場所と IScannerInfo に設定したインクルード検索パスをヒントにして実際のヘッダーファイルを探し、 #include をきちんと処理してくれます。

基本的にはこれで良いのですが、僕のプラグインの作り方が特殊なため、不具合がでるのです。僕はプラグインを Eclipse のプラグインとしてではなく、通常のJavaアプリとして開発しているため、プラグインの実行時にはワークスペースが開かれていない状態となります(Eclipse 上で動いていないので当たり前)。
その状態で SavedFilesProvider を使うと
java.lang.IllegalStateException: Workspace is closed.
at org.eclipse.core.resources.ResourcesPlugin.getWorkspace(ResourcesPlugin.java:399)
なる例外が発生してしまいます。

SavedFilesProvider はユーザーのヘッダファイルも探してくれる機能を持っているため、ワークスペースが開かれていないとユーザーのヘッダファイルを探せなくて例外が出ます。困りました。これでは単体テストができないじゃないですか。
さらに僕がやりたいのは、GCCの汚れたヘッダではなく、 ANSI-C に準拠するミニマムなヘッダを食わせてパースしたいんです。

そこで、僕が考えた解決策は InternalFileContentProvider (SavedFilesProvider の親クラス)を継承して、独自のプロバイダを書くことです。このクラスはどうやら内部使用が前提のクラスらしく、継承元に指定しようとすると Discouraged access などと警告が出るので気持ち悪いですが、エラーにはなりませんので無視します。
public class MyFileContentProvider extends InternalFileContentProvider {
    final private Map<String, char[]> stdHeaders;

    public MyFileContentProvider() {
        stdHeaders = new HashMap<String, char[]>();
        stdHeaders.put("/usr/include/stdio.h", "int puts(const char* s);".toCharArray());
        stdHeaders.put("/usr/include/stdlib.h", "".toCharArray());
    }

    @SuppressWarnings("restriction")
    @Override
    public InternalFileContent getContentForInclusion(String filePath,
            IMacroDictionary macroDictionary) {
        if (!getInclusionExists(filePath)) {
            return null;
        }

        if (stdHeaders.containsKey(filePath)) {
            return (InternalFileContent) FileContent.create(filePath, stdHeaders.get(filePath));
        }
        return SavedFilesProvider.getInstance().getContentForInclusion(filePath, macroDictionary);
    }

    @SuppressWarnings("restriction")
    @Override
    public InternalFileContent getContentForInclusion(IIndexFileLocation ifl,
            String astPath) {
        return SavedFilesProvider.getInstance().getContentForInclusion(ifl, astPath);
    }

}
結局作ったのがこんなクラスです。まだ製作途中なので標準ヘッダの中身が超適当ですが、これを拡充していけばいけるはずです。
ポイントは

  • 標準ライブラリのファイル名が来たら、組み込みの文字列からFileContentを作って返す
  • それ以外のファイル名がきたら、 SavedFilesProvider に委譲する
の2点です。結局 SavedFilesProvider を使ってしまっていますが、単体テストでは標準ライブラリだけを使うようにすれば、 SavedFilesProvider は呼び出されませんので、例外が発生しません。

以上、ヘッダファイルを考慮に入れた CDT でのパース方法でした。

0 件のコメント:

コメントを投稿