さくらんぼのlambda日記

lambdaちっくなことからゲーム開発までいろいろ書きます。

clangで相互参照の情報を取得する!

前回からだいぶ間が開いてますが、やっと解析の方法をまとめることができそうなのでまとめます。目標は「clangのIndexerを使ってコードの相互参照の情報を取得する」です。

  1. Indexerを作る
  2. IndexerにTranslationUnitを複数個渡していく
  3. IndexerからAnalyzerを作成
  4. Analyzerで参照の情報を取得

結論から言うと、前回予想したこの手順で問題ありませんでした。

手順の上で大事なところはTranslationUnitを作成する方法です。他の手順はインスタンスを作成すれば良いので、変数宣言さえ出来れば問題ないです。

TranslationUnitの準備

まず、clang::idx::TranslationUnit を作成したいです。しかし、このクラスは仮想クラスです、なので継承してインスタンス化が可能なクラスを定義する必要があります。

#pragma once
#include "llvm/ADT/IntrusiveRefCntPtr.h"
#include "llvm/Support/CommandLine.h"

#include "clang/Frontend/ASTUnit.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Index/Indexer.h"
#include "clang/Index/Program.h"
#include "clang/Index/TranslationUnit.h"
#include "clang/Index/DeclReferenceMap.h"
#include "clang/Index/SelectorMap.h"
#include "clang/Index/Handlers.h"
#include "clang/Index/Analyzer.h"

#include <clang/AST/ASTConsumer.h>
#include <clang/AST/Decl.h>
#include <clang/AST/DeclTemplate.h>
#include <clang/Parse/ParseAST.h>
#include <clang/Basic/TargetInfo.h>


class ASTUnitTU : public clang::idx::TranslationUnit {
  clang::ASTUnit *AST;
  clang::idx::DeclReferenceMap DeclRefMap;
  clang::idx::SelectorMap SelMap;

 public:
  ASTUnitTU(clang::ASTUnit *ast)
      : AST(ast),DeclRefMap(ast->getASTContext()),SelMap(ast->getASTContext())  {

  }

  virtual clang::ASTContext &getASTContext() {
    return AST->getASTContext();
  }

  virtual clang::Preprocessor &getPreprocessor() {
    return AST->getPreprocessor();
  }

  virtual clang::DiagnosticsEngine &getDiagnostic() {
    return AST->getDiagnostics();
  }

  virtual clang::DeclReferenceMap &getDeclReferenceMap() {
    return DeclRefMap;
  }

  virtual clang::SelectorMap &getSelectorMap() {
    return SelMap;
  }
};

こんな感じに定義しておけば良いです。このクラスはclangのIndexerを利用しているサンプルから持って来ました。


ASTUnitをソースから生成

ASTUnitの定義でTranslationUnitの準備は出来ました。あとはclangの機能で作成していくだけです。

まず、clangのコンパイラを使うためにclang::CompilerInstanceを生成します

  clang::CompilerInstance compiler;

このクラスに、clangを利用してコンパイルするためのすべての機能が含まれています。

次に、コンパイルエラーが発生した場合などに対処するためのDiagnosticEngineを生成します。今回の解析では、コンパイルエラーは無視して欲しいのでclang::IgnoringDiagConsumerを生成します。また、setUpDiagnosticOptsにてDiagnosticEngineのオプションを設定しています。基本的にデフォルトの設定にしてあります。変更したいときはヘッダなどに定義が書いてあるので、参考にすると良いと思います。...が解説も何も書いて無いものもあるのでデフォルトで使っていれば良いと思います。

setUpDiagnosticOptsの定義。

// setup DiagnosticOptions
void setUpDiagnosticOpts(clang::DiagnosticOptions& diagOpts) {
  diagOpts.ErrorLimit = 60000;
  diagOpts.IgnoreWarnings = 1;
  diagOpts.TabStop = clang::DiagnosticOptions::DefaultTabStop;
  diagOpts.MessageLength = 0;
  diagOpts.NoRewriteMacros = 0;
  diagOpts.Pedantic = 0;
  diagOpts.PedanticErrors = 0;
  diagOpts.ShowCarets = 1;
  diagOpts.ShowColors = 0;
  diagOpts.ShowOverloads = clang::DiagnosticsEngine::Ovl_All;
  diagOpts.ShowColumn = 1;
  diagOpts.ShowFixits = 1;
  diagOpts.ShowLocation = 1;
  diagOpts.ShowOptionNames = 0;
  diagOpts.ShowCategories = 0;
  diagOpts.ShowSourceRanges = 0;
  diagOpts.ShowParseableFixits = 0;
  diagOpts.VerifyDiagnostics = 0;
  diagOpts.TemplateBacktraceLimit = clang::DiagnosticOptions::DefaultTemplateBacktraceLimit;
  diagOpts.MacroBacktraceLimit = clang::DiagnosticOptions::DefaultMacroBacktraceLimit;
}

実際にDiagnosticEngineを作成するコード。

  // Setup Diagnostic Options
  clang::DiagnosticOptions diagOpts;
  setUpDiagnosticOpts(diagOpts);

  // Setup DiagnosticEngine
  auto ignoreConsumer = new clang::IgnoringDiagConsumer();

  // 入力されているファイルの数+1。今回は引数はファイル1つなので2を指定。
  const int InputFileNum = 2;  

  // コンパイラに与える引数を設定
  const char* compilerArgments[] = {"analyzer",targetSource.c_str()};

  auto diagnosticEngine = compiler.createDiagnostics(diagOpts,InputFileNum,compilerArgments,ignoreConsumer);

  // コンパイラにDiagnosticEngineを設定
  compiler.setDiagnostics(diagnosticEngine.getPtr());


次に、実際にコンパイルを行なってくれるInvocationを生成します。これはCompilerInstanceから生成できます。
なお、setUpLangOpts はコンパイラの設定を指定するために定義した関数です。
ここも、デフォルトの値を設定しています。変更したいときはヘッダなどに定義が書いてあるので、参考にすると良いと思います。...が解説も何も書いて無いもの(以下略

setUpLangOptsの定義です。

void setUpLangOpts(clang::LangOptions& langOpts) {
  langOpts.NoInline = 0;
  //langOpts.BCPLComment = true;
  langOpts.Bool   = true;
  //langOpts.MicrosoftExt = true;
  //langOpts.MicrosoftMode = true;
  langOpts.CPlusPlus  = true;
  langOpts.CPlusPlus0x = true;
  langOpts.Exceptions  = true;
  langOpts.CXXExceptions = true;
  langOpts.MSBitfields = true;
  langOpts.NeXTRuntime = false;
  langOpts.NoBuiltin  = true;
  langOpts.CatchUndefined = true;
  langOpts.EmitAllDecls = true;
  //langOpts.MSCVersion  = _MSC_VER;
}

実際にInvocationを作成するコード。

  /*============================
    Invocationを作成
    =====================*/
  // CompilerInstanceからInvocationを作成
  auto& invocation = compiler.getInvocation();

  // setup language options
  clang::LangOptions langOpts;
  setUpLangOpts(langOpts);
  invocation.setLangDefaults(langOpts,clang::IK_C);

  auto& diag = compiler.getDiagnostics();
  auto depOpts = invocation.getDependencyOutputOpts();
  depOpts.UsePhonyTargets = 1;

  clang::CompilerInvocation::CreateFromArgs(invocation,
                                            compilerArgments + 1,
                                            compilerArgments + 2,
                                            diag);

次に、以下の設定を行う必要があります。

  • コンパイルのターゲットを設定
  • ファイル、ソースなどの行、列数といった情報を管理するためにマネージャ2つを作成
  • インクルードファイルの追加
  • ASTの情報を格納するASTContextを作成

そろそろ設定ばっかりで嫌になりそうな感じですがもう少しでASTが生成できる準備が整います。

/* インクルードパスを追加する関数 */
void addIncludeFiles(clang::CompilerInstance& compiler,std::vector<std::string> lines) {
  std::vector<std::string>::iterator itr;
  for(itr=lines.begin();itr!=lines.end();++itr) {
    compiler.getHeaderSearchOpts().AddPath(itr->c_str(),
                                           clang::frontend::Quoted, true, false, false);
    compiler.getHeaderSearchOpts().AddPath(itr->c_str(),
                                           clang::frontend::Angled, true, false, false);
    std::cout <<  "Include:"  << itr->c_str() << std::endl;
  }
}
  // コンパイルのターゲットを設定
  compiler.setTarget(clang::TargetInfo::CreateTargetInfo(diag,compiler.getTargetOpts()));

  // ファイル、ソースなどの行、列数といった情報を管理するためにマネージャ2つを作成
  compiler.createFileManager();
  compiler.createSourceManager(compiler.getFileManager());

  // インクルードファイルを設定
  addIncludeFiles(compiler,includes);

  // プリプロセッサの作成
 compiler.createPreprocessor();

  // ASTの情報を格納するASTContextを作成
  compiler.createASTContext();

  // 意味解析した結果を格納するSemaを作成
  compiler.createSema(clang::TU_Complete, NULL);

さて、ハンドラの登録とParseASTを実行して解析しましょう。解析が完了したらinvocationからASTUnitを作成してくれるclang::ASTUnit::LoadFromCompilerInvocation関数を呼べば目標の「TranslationUnitを作成する」を達成できます。やったー。

  // ASTをParseする時に呼んで欲しいイベントハンドラを登録
  compiler.setASTConsumer(new MyASTConsumer());

  // To Parse C Source file from ARG
  auto& inputs = compiler.getFrontendOpts().Inputs;
  if (inputs.size() > 0) {
    compiler.InitializeSourceManager(inputs[0].second);
    clang::ParseAST(
        compiler.getPreprocessor(),
        &compiler.getASTConsumer(),
        compiler.getASTContext()
                    );
  }
  // ASTUnitを作成
  auto astUnit = clang::ASTUnit::LoadFromCompilerInvocation(&invocation,
                                                            llvm::IntrusiveRefCntPtr<clang::DiagnosticsEngine>(&diag));

Indexerを使ってみる

今まで手順でファイルからASTUnitが作成できました。上の手順を各ファイルごとに実行し、ASTUnitの配列などを作成しておきましょう。そしてそれを使ってIndexerを使ってみましょう。

std::vector<clang::ASTUnit*> astList;

std::vector<std::string>::iterator itr;
for(itr=targetSources.begin();itr!=targetSources.end();++itr) {
  // 上記の手順でASTUnitを作成する関数
  llvm::OwningPtr<clang::ASTUnit>  ast(generateASTUnitFromSource(itr->c_str(),includeSources));
  astList.push_back(ast.take());
}

// ProgramとIndexerを作成する
clang::idx::Program Prog;
clang::idx::Indexer Idxer(Prog);

// IndexerにIndexしてもらう。
for (unsigned i = 0, e = astList.size(); i != e; ++i) {
  ASTUnitTU *TU = new ASTUnitTU(astList[i]);
  Idxer.IndexAST(TU);
}


これで、相互参照の情報が完成しました。この情報を使って参照情報を取得してみましょう。

相互参照情報の取得

情報の取得にはAnalyzerを使います。

clang::idx::Analyzer* analyzer = new clang::idx::Analyzer(Idxer.getProgram(),Idxer);

このAnalyzerを使えば、このような形で宣言と参照している箇所などの情報を取得できます。

analyzer->FindDeclarations(pDecl,*handler);
analyzer->FindReferences(pDecl,*handler);

第一引数には,調べたい対象の宣言を与えます。
第二引数のhandlerにはハンドラーとなるTULocationHandlerクラスの派生クラスを登録します。

class Handler : public TULocationHandler {
   public:
     Handler(){};
  private:
    void Handle(TULocation TULoc) {
      // TULocには場所の情報が格納されている
      // FindDeclarationsだと宣言箇所
      // FindReferencesだと呼び出し箇所
    };
};

auto handler = new Handler();
analyzer->FindDeclarations(pDecl,*handler);
analyzer->FindReferences(pDecl,*handler);

なお、宣言の取得の方法としては

  1. 文字列からEntityを作成し、そこからDeclを取得
  2. ParseASTの時のハンドラで記憶しておく

の二種類が考えられます。

文字列から取得する場合は以下のようにします。
ParseASTの際のハンドラは適当に皆さんの好きなように実装するのが良いでしょう。

for (unsigned i = 0, e = astList.size(); i != e; ++i) {
  clang::idx::Entity Ent = clang::idx::Entity::get("func", Idxer.getProgram());
  auto decl = Ent.getDecl(astList[i]->getASTContext());
  if (decl) {
      Handler* handler = new Handler();
      analyzer->FindReferences(decl,*handler);
  }
}


それではみなさんも楽しいclangライフを。