2011年11月30日水曜日

Clangでマクロの展開箇所を調べる

C++ソースコード上にマクロで表現されたDSLをclangで調べるために、マクロ展開箇所を調べたくなった。

マクロはclang::Preprocessorの範疇であるが、Preprocessorはマクロの展開情報を直接扱わない。clangにはPreprocessingRecordおよびPreprocessedEntryというクラスがあり、これらを利用する。

まず、Preprocessorが処理においてPreprocessingEntryを保存してくれるよう、PreprocessingRecordをセットアップする。
clang::InitializePreprocessor(pp, ppOpts, headerSearchOpts, frontendOpts);
pp.createPreprocessingRecord(true);

そうすると、プリプロセッシングの後でPreprocessorに設定されたPreprocessingRecordが、PreprocessedEntryを反復してくれる。PreprocessedEntryの派生クラスとしてMacroExpansionがあるので、キャストして情報を得ればよい。
clang::PreprocessingRecord* ppRecord = pp.getPreprocessingRecord();
clang::PreprocessingRecord::iterator ppr = ppRecord->begin();
clang::PreprocessingRecord::iterator eoppr = ppRecord->end();
for(; ppr != eoppr; ++ppr)
{
    if((*ppr)->getKind() == clang::PreprocessedEntity::MacroExpansionKind)
    {
        clang::MacroExpansion* expansion
            = llvm::dyn_cast<clang::MacroExpansion>(*ppr);
        if(expansion->getName()->getName() == "MY_MACRO")
        {
            clang::SourceRange range = expansion->getSourceRange();
            range.getBegin().dump(srcMgr);
        }
    }
}

2011年11月20日日曜日

CLangでクラスの中身を調べる:所属パッケージを調べる

前回までで、複数のcppをまとめて処理することが(たぶん)できないので、列挙されたクラスをあとで重複排除する必要があることがわかった。

clangにはリンク等の時点で判別する方法があると思うが、その機能を調べるのが非常にめんどくさい。そこで、各クラスの所属パッケージを調べておいて、その名前を文字列比較することで判定しようと思う。

自分の名前(クラス名)はCXXRecordDecl::getName()で取得できる。なので所属するパッケージ(名前空間)を取得できればよいはずである。

名前空間はclang::Type::dump()にてダンプされているので、そこで利用されているルーチンclang::TypePrinter::AppendScope()を調べてみた。以下のような実装である:

void TypePrinter::AppendScope(DeclContext *DC, std::string &Buffer) {
  if (DC->isTranslationUnit()) return;
  AppendScope(DC->getParent(), Buffer);

  unsigned OldSize = Buffer.size();

  if (NamespaceDecl *NS = dyn_cast<NamespaceDecl>(DC)) {
    if (NS->getIdentifier())
      Buffer += NS->getNameAsString();
    else
      Buffer += "<anonymous>";
  } else if (ClassTemplateSpecializationDecl *Spec
               = dyn_cast<ClassTemplateSpecializationDecl>(DC)) {
    IncludeStrongLifetimeRAII Strong(Policy);
    const TemplateArgumentList &TemplateArgs = Spec->getTemplateArgs();
    std::string TemplateArgsStr
      = TemplateSpecializationType::PrintTemplateArgumentList(
                                            TemplateArgs.data(),
                                            TemplateArgs.size(),
                                            Policy);
    Buffer += Spec->getIdentifier()->getName();
    Buffer += TemplateArgsStr;
  } else if (TagDecl *Tag = dyn_cast<TagDecl>(DC)) {
    if (TypedefNameDecl *Typedef = Tag->getTypedefNameForAnonDecl())
      Buffer += Typedef->getIdentifier()->getName();
    else if (Tag->getIdentifier())
      Buffer += Tag->getIdentifier()->getName();
  }

  if (Buffer.size() != OldSize)
    Buffer += "::";
}

本来の目的を考えると、上のルーチンをそのまま使えばいいのだが、それはできない。このTypePrinterは匿名の名前空間(namespace {} と宣言されているやつ)内で定義されており、かつAST/TypePrinter.cppにクラス定義があるのである。
なのでこのルーチンを解析して移植する。
std::string getPackagePath(clang::DeclContext* context)
{
    if(context == nullptr)
    {
        return "";
    }

    if(context->isTranslationUnit())
    {
        return "";
    }

    std::string path = getPackagePath(context->getParent());
    std::string::size_type oldLen = path.length();

    clang::NamespaceDecl* nsDecl = llvm::dyn_cast<clang::NamespaceDecl>(context);
    if(nsDecl == nullptr)
    {
        clang::TagDecl* tag = llvm::dyn_cast<clang::TagDecl>(context);
        if(tag != nullptr)
        {
            clang::TypedefNameDecl* tdef = tag->getTypedefNameForAnonDecl();
            if(tdef == nullptr)
            {
                path += tag->getIdentifier()->getName();
            }
            else
            {
                path += tdef->getIdentifier()->getName();
            }
        }
    }
    else
    {
        if(nsDecl->getIdentifier() == nullptr)
        {
            path += "<anonymous>";
        }
        else
        {
            path += nsDecl->getName();
        }
    }

    if(path.length() != oldLen)
    {
        path += "::";
    }

    return path;
}

これで通常の名前空間やインナークラスは処理できるようになった。TypePrinter側の実装でテンプレート用?ルーチンがあるのが気になる。

追記:
クラステンプレートの定義はASTContextによるclang::Typeの反復においては反復されない。

CLangでクラスの中身を調べる:複数のソースコードをまとめてパース・・・はできない

前回で単cppの重複定義を取り除くことができた。が、複数h/cppをパースさせた場合にどうなるのか心配になってきた。
なので今回は複数のh/cppを入力してみる。

cppをテキトーに追加してみる

ソースコードをメモリに展開するのはclang::SourceManagerである。しかしSourceManagerに(mainではない)ソースファイルを追加するであろうcreateFileID()は、インクルードパスのSourceLocationを要求する。createMainFileID()は要求しないのに。

この違いは何かと思って、createMainFileID()を使わずに以下のようにファイルを追加してみる。

srcMgr.createFileID(fileMgr.getFile("D:\\develop\\Clang\\SampleProj\\Package1\\Super.cpp"),
                    clang::SourceLocation(), clang::SrcMgr::C_User);

すると以下のようなエラーメッセージが出てしまう。

>fatal error: error opening file '<invalid loc>':

うーん、そうですか。ダメですか。でもcreateMainFileID()を複数回呼ぶのもダメですよね。

じゃあってことで、1つめのコードはcreateMainFileID()を、2つめ以降はcreateFileID()を使ってみる。

srcMgr.createMainFileID(fileMgr.getFile("D:\\develop\\Clang\\SampleProj\\Package1\\Super.cpp"));
srcMgr.createFileID(fileMgr.getFile("D:\\develop\\Clang\\SampleProj\\Package2\\Sub.cpp"),
                    clang::SourceLocation(), clang::SrcMgr::C_User);

実行すると、Sub.cppは解析結果に含まれない。

SourceLocationの生成を調べる

SourceLocationがデフォルト生成のままだとダメなのか、と思ってSourceLocationを調べると、
  • メンバ変数はunsigned ID1つだけ。
  • コンストラクタは引数なし。
  • IDを初期化できそうなメンバ関数はない。
となっている。つまりSourceLocation自身は初期化機能を提供せず、他のクラスが初期化するのだ。

これまで使ってきたクラスの中から生成しそうなやつをピックアップすると
  • FileManager
  • HeaderSearch
  • SourceManager
くらいなのだが、このうちFileManagerとHeaderSearchはSourceLocationを全く参照していない。なのでSourceManager.hをあさってみる。
  • clang::SrcMgrのインナークラスであるFileInfoにgetIncludeLoc()というAPIがある。
  • SourceManager自身にgetIncludeLoc()というAPIがある。
とりあえず後者を採用して実行・・・

clang::FileID mainId = srcMgr.createMainFileID(fileMgr.getFile("D:\\develop\\Clang\\SampleProj\\Package1\\Super.cpp"));
srcMgr.createFileID(fileMgr.getFile("D:\\develop\\Clang\\SampleProj\\Package2\\Sub.cpp"),
                    srcMgr.getIncludeLoc(mainId), clang::SrcMgr::C_User);

結果、やっぱりダメ。SourceLocationの問題ではないらしい。

複数cppをまとめてコンパイルできると誰が言った?

ここであることが頭をよぎる。gcc等では1回の起動で1つの.cppしか処理しない。clangのクラス群もそのように設計されているのではないか?
SourceManagerのMainという概念は、.cpp1ファイルのことを指すのではないか?

もしそうだとすると、非常に面倒なことである。ASTContextにおけるclang::Typeの反復の重複除去も意味がない。
そこで、1つのASTContextで複数の.cppを処理できるのか、試してみることにする。

まずSourceManagerのリセットである。調べたところ、MainFileIDはclearIDTable()メンバ関数でのみリセットされるようである。

ということで、以下のように実装してみる。

clang::IdentifierTable identifierTable(langOpts);
clang::SelectorTable selectorTable;
clang::Builtin::Context builtinContext;
clang::ASTContext astContext(langOpts, srcMgr, targetInfo, identifierTable,
                                selectorTable, builtinContext, 0);

clang::ASTConsumer astConsumer;
clang::Sema sema(pp, astContext, astConsumer);
sema.Initialize();

const char* targets[2] =
{
    "D:\\develop\\Clang\\SampleProj\\Package1\\Super.cpp",
    "D:\\develop\\Clang\\SampleProj\\Package2\\Sub.cpp"
};
for(const char** target = targets; target != (targets + 2); ++target)
{
    srcMgr.clearIDTables();
    srcMgr.createMainFileID(fileMgr.getFile(*target));
    clang::ParseAST(pp, &astConsumer, astContext);
}

// 構文解析結果を確認する。
clang::ASTContext::const_type_iterator t = astContext.types_begin();
clang::ASTContext::const_type_iterator eot = astContext.types_end();
for(; t != eot; ++t)
{
    if((*t)->getTypeClass() != clang::Type::Record)
    {
        continue;
    }

    clang::CXXRecordDecl* pDecl = (*t)->getAsCXXRecordDecl();
    if( (pDecl != nullptr) && pDecl->hasDefinition() && pDecl->isClass() )
    {
        std::printf("\n---------------------------------------------------------\n");

        (*t)->dump();
    }
}

実行した結果、次のようなエラーメッセージが。

Assertion failed: NumEnteredSourceFiles == 0 && "Cannot reenter the main file!", file D:\develop\Clang\llvm\tools\clang\lib\Lex\Preprocessor.cpp, line 388

うーむ、Preprocessorの時点で再利用できないのね。

CLangでクラスの中身を調べる:クラスの重複反復を抑制する

ちょっと時間が空いた。llvmとclangを3.0 RC4にして継続。

clang::ASTContext::types_begin()だと、クラスが重複して反復されてしまう。これをなんとかしたい。でもclang::Typeやclang::CXXRecordDeclにはoperator==()がないので、反復済みかどうかの判定では対処できない。

clang::Typeのメンバ関数に、getTypeClass()というものがあった。これを調べてみると重複されて反復されるclang::Typeは片方がRecordでもう片方がElaboratedであるようだ。
clang::ASTContext::const_type_iterator t = astContext.types_begin();
clang::ASTContext::const_type_iterator eot = astContext.types_end();
for(; t != eot; ++t)
{
    if((*t)->getTypeClass() == clang::Type::Elaborated)
    {
        continue;
    }

これでOKになった。clang::Type::Recordのみ通すようにすれば確実性は上がるだろう。
※問題は、これが何を意味するのかさっぱり理解していないところだ・・・。

ところで、今年12/3のboost勉強会(#7)ではclangやるセッションがいくつかあるのね・・・。カミさんと日程調整していたら参加が締め切られてorz。