LLVM Clang ASTでC++の構文解析をする

LLVMのClang ASTを用いてC++の構文解析ツールを作成する方法を紹介します。Clang ASTに関しては、日本語のまとまったドキュメントは殆どなく、公式サイトの情報も触り部分だけで、深い所まで使おうと思うと、Clangのソースコードを読み必要がありました。

簡単な解説とサンプルコードを交えて、Clang ASTの使い方を紹介したいと思います。

環境構築

  • Windows 10
  • Visual Studio Coomunity 2019(MSVC)
  • LLVM、Clang 11.0.0

LLVM、Clangをビルド

Clang ASTを利用するのに必要なプロジェクトをビルドします。

Visual StudioでLLVM、Clangをビルドする方法はこちらの記事で紹介しています。

LLVM、ClangをMSBuildかNinjaでビルドします。Ninjaの方がビルド時間が短いのでお勧めです。Ninjaでのビルド方法はこちらの記事で紹介しています。

ここではMSBuildでビルドする時のコマンドを紹介します。CMakeでVisual Studio 2019のプロジェクト作成した後に、MSBuildでビルドしていきます。

for /f "usebackq delims=" %%a in (`"%ProgramFiles(x86)%/Microsoft Visual Studio/Installer/vswhere" -version 16 -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set VSPATH=%%a
call "%VSPATH%/VC/Auxiliary/Build/vcvars64.bat"

cmake -G "Visual Studio 16 2019" -Thost=x64 -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_CXX_FLAGS_RELEASE:STRING="/MT" -DCMAKE_CXX_FLAGS_DEBUG:STRING="/MTd" -DCMAKE_INSTALL_PREFIX:PATH="../install/release"

MSBuild.exe tools\clang\lib\Analysis\clangAnalysis.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\AST\clangAST.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Basic\clangBasic.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Driver\clangDriver.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Edit\clangEdit.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Frontend\clangFrontend.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Lex\clangLex.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Parse\clangParse.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Rewrite\clangRewrite.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Frontend\Rewrite\clangRewriteFrontend.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Sema\clangSema.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Serialization\clangSerialization.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe tools\clang\lib\Tooling\clangTooling.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\BinaryFormat\LLVMBinaryFormat.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\Bitcode\Reader\LLVMBitReader.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\Bitcode\Bitstream\Reader\LLVMBitstreamReader.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\IR\LLVMCore.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\DebugInfo\PDB\LLVMDebugInfoPDB.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\Demangle\LLVMDemangle.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\Frontend\OpenMP\LLVMFrontendOpenMP.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\MC\LLVMMC.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\MC\MCParser\LLVMMCParser.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\Option\LLVMOption.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\ProfileData\LLVMProfileData.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\Remarks\LLVMRemarks.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"
MSBuild.exe lib\Support\LLVMSupport.vcxproj /t:build /p:configuration=Debug /p:Platform="x64"

MSBuild.exe tools\clang\lib\Analysis\clangAnalysis.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\AST\clangAST.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Basic\clangBasic.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Driver\clangDriver.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Edit\clangEdit.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Frontend\clangFrontend.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Lex\clangLex.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Parse\clangParse.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Rewrite\clangRewrite.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Frontend\Rewrite\clangRewriteFrontend.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Sema\clangSema.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Serialization\clangSerialization.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe tools\clang\lib\Tooling\clangTooling.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\BinaryFormat\LLVMBinaryFormat.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\Bitcode\Reader\LLVMBitReader.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\Bitcode\Bitstream\Reader\LLVMBitstreamReader.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\IR\LLVMCore.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\DebugInfo\PDB\LLVMDebugInfoPDB.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\Demangle\LLVMDemangle.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\Frontend\OpenMP\LLVMFrontendOpenMP.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\MC\LLVMMC.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\MC\MCParser\LLVMMCParser.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\Option\LLVMOption.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\ProfileData\LLVMProfileData.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\Remarks\LLVMRemarks.vcxproj /t:build /p:configuration=Release /p:Platform="x64"
MSBuild.exe lib\Support\LLVMSupport.vcxproj /t:build /p:configuration=Release /p:Platform="x64"

ClangASTに必要なライブラリのリンク設定

Visual StudioプロジェクトのProperty > C++ > General > Additional Include Directories で include pathを追加。

$(ProjectDir)llvm/tools/clang/include
$(ProjectDir)llvm/include

Visual StudioのプロジェクトのProperty > Linker > General > Additional Library Directires に、libへのpathを追加。

llvm/Debug/lib // Debugビルド
llvm/Release/lib // Releaseビルド

Visual StudioのプロジェクトのProperty > Linker > Input > Additional Dependencies に lib を追加。

clangAnalysis.lib
clangAST.lib
clangBasic.lib
clangDriver.lib
clangEdit.lib
clangFrontend.lib
clangLex.lib
clangParse.lib
clangRewrite.lib
clangRewriteFrontend.lib
clangSema.lib
clangSerialization.lib
clangTooling.lib
LLVMBinaryFormat.lib
LLVMBitReader.lib
LLVMBitstreamReader.lib
LLVMCore.lib
LLVMDebugInfoPDB.lib
LLVMDemangle.lib
LLVMFrontendOpenMP.lib
LLVMMC.lib
LLVMMCParser.lib
LLVMOption.lib
LLVMProfileData.lib
LLVMRemarks.lib
LLVMSupport.lib

Clang AST

Clang ASTの処理の流れ

処理の流れが分かっていないと、理解が難しいと思うので、最初に説明します。

clang::tooling::ClangToolが、clang::tooling::FrontendActionFactoryを受け取って処理を開始します。

clang::tooling::FrontendActionFactoryは、clang::FrontendActionを生成して返すだけの実装です。

Clangは、LLVMのFrontendの実装なのでFrontendActionとして機能を実装します。

今回は、コンパイルではなく、ASTでの構文解析をしたいので、clang::ASTFrontendActionを生成するclang::tooling::FrontendActionFactoryを渡します

clang::ASTFrontendActionは、構文解析をするとvirtual関数を呼び出します

virtual void clang::ASTConsumer::HandleTranslationUnit(clang::ASTContext &context);

clang::ASTContextを解析するには、clang::RecursiveASTVisitorを使います。clang::RecursiveASTVisitorはそれぞれの要素に従って、virtual関数を呼び出します。この関数の中で、必要な要素を取り出す事が出来ます。

virtual bool clang::RecursiveASTVisitor::VisitNamespaceDecl(clang::NamespaceDecl *decl);
virtual bool clang::RecursiveASTVisitor::VisitFunctionDecl(clang::FunctionDecl *decl);
virtual bool clang::RecursiveASTVisitor::VisitFieldDecl(clang::FieldDecl *decl);

clang::tooling::ClangToolを使う準備をする

最初の課題は、clang::tooling::ClangToolに渡す引数の生成です。
clang::tooling::CommonOptionsParser のインスタンスを生成します。argc, argvの形にオプションを整形して渡すだけです。気を付けるべきポイントは、空文字列、コンパイルファイルパス、”–“、”clang++”、その他のオプションの順番で渡す事です。

llvm::cl::OptionCategory は -help 用なので、特に使いません。

CommandLine 2.0 Library Manual — LLVM 13 documentation

bool Analyzer::Analyze(const char *compile_file,
                       const std::vector<const char *> &include_paths,
                       const char *base_class) {
  assert(compile_file);

  if (base_class) {
    base_class_ = base_class;
  }

  options_.emplace_back("--");
  options_.emplace_back("clang++");
  for (const char *path : include_paths) {
    options_.emplace_back(path);
  }

  // Generate command line options.
  assert(compile_file && strlen(compile_file) > 0);
  int argc = 2;
  argc += static_cast<int>(options_.size());

  std::unique_ptr<const char *> argv(new const char *[argc]);
  argv.get()[0] = "";
  argv.get()[1] = compile_file;
  int index = 2;
  for (std::string &option : options_) {
    assert(index < argc);
    argv.get()[index] = option.c_str();
    ++index;
  }

  llvm::cl::OptionCategory option("");
  clang::tooling::CommonOptionsParser op(argc, argv.get(), option);
  clang::tooling::ClangTool tool(op.getCompilations(), op.getSourcePathList());

  // Run clang tooling
  tool.run(newFrontendActionFactory<ClangASTFrontendAction>(this).get());

  return true;
}

clang::tooling::FrontendActionFactoryの実装

clang::ASTFrontendActionを継承して、独自のFrontendActionを返す処理を実装します。

template <typename T>
std::unique_ptr<clang::tooling::FrontendActionFactory> newFrontendActionFactory(
    Analyzer *analyzer) {
  class ClangASTFrontendActionFactory
      : public clang::tooling::FrontendActionFactory {
   public:
    ClangASTFrontendActionFactory(Analyzer *analyzer) : analyzer_(analyzer) {}

    std::unique_ptr<clang::FrontendAction> create() override {
      return std::unique_ptr<clang::FrontendAction>(
          new ClangASTFrontendAction(analyzer_));
    }

   private:
    Analyzer *analyzer_{nullptr};
  };

  return std::unique_ptr<clang::tooling::FrontendActionFactory>(
      new ClangASTFrontendActionFactory(analyzer));
}

clang::FrontendAction、clang::ASTConsumerの実装

clang::FrontendActionを継承して、独自のFrontendActionを実装します。FrontendActionは、clang::ASTConsumerを引数として取り、内部的にclang::ASTConsumerを呼び出します。

class ClangASTConsumer : public clang::ASTConsumer {
 public:
  explicit ClangASTConsumer(clang::CompilerInstance &ci, Analyzer *analyzer)
      : visitor_(ci.getASTContext(), analyzer) {}
  virtual ~ClangASTConsumer() {}

  virtual void HandleTranslationUnit(clang::ASTContext &context) override {
    visitor_.TraverseDecl(context.getTranslationUnitDecl());
  }

 private:
  ClangASTVisitor visitor_;
};

class ClangASTFrontendAction : public clang::ASTFrontendAction {
 public:
  ClangASTFrontendAction(Analyzer *analyzer) : analyzer_(analyzer) {}
   virtual ~ClangASTFrontendAction() {}

  virtual std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
      clang::CompilerInstance &ci, clang::StringRef) override {
    return std::unique_ptr<clang::ASTConsumer>(
        new ClangASTConsumer(ci, analyzer_));
  }

 private:
  Analyzer *analyzer_{nullptr};
};

clang::RecursiveASTVisitorの実装

lang::RecursiveASTVisitorを継承して、構文解析時にvirtual関数が呼びされるようにします。サンプルプログラムでは、特定の基底クラスを持つクラスの情報を収集しています。

/// Clang AST Visitor implement class.
class ClangASTVisitor : public clang::RecursiveASTVisitor<ClangASTVisitor> {
 public:
  explicit ClangASTVisitor(clang::ASTContext &context, Analyzer *analyzer)
      : context_(context), analyzer_(analyzer) {}
  virtual ~ClangASTVisitor() {}

  bool VisitNamespaceDecl(clang::NamespaceDecl * /*decl*/) {
    //std::string name = decl->getName();
    //name.c_str();

    return true;
  }

  bool VisitCXXRecordDecl(clang::CXXRecordDecl *decl) {
    if (IsSpecifiedBaseClass(decl)) {
      object_decl_ = decl;
    } else {
      if (object_decl_ && decl->isThisDeclarationADefinition() &&
                              decl->isDerivedFrom(object_decl_)) {
        analyzer_->AddCXXRecord(decl);
      }
    }

    return true;
  }

  bool VisitFunctionDecl(clang::FunctionDecl *decl) {
    clang::DeclContext *parent = decl->getParent();
    if (parent) {
      clang::Decl::Kind kind = parent->getDeclKind();
      if (kind == clang::Decl::Kind::CXXRecord) {
        clang::CXXRecordDecl *cxx_record =
            static_cast<clang::CXXRecordDecl *>(parent);
        analyzer_->AddFunction(cxx_record, decl);
      }
    }

    return true;
  }

  bool VisitFieldDecl(clang::FieldDecl *decl) {
    clang::DeclContext *parent = decl->getParent();
    if (parent) {
      clang::Decl::Kind kind = parent->getDeclKind();
      if (kind == clang::Decl::Kind::CXXRecord) {
        clang::CXXRecordDecl *cxx_record =
            static_cast<clang::CXXRecordDecl *>(parent);
        analyzer_->AddField(cxx_record, decl);
      }
    }

    return true;
  }

 private:
  /// Return true when CXXRecordDecl is specified base class,
  /// Otherwise return false.
  bool IsSpecifiedBaseClass(clang::CXXRecordDecl *decl) {
    std::string name = decl->getQualifiedNameAsString();
    if (analyzer_->base_class() == decl->getQualifiedNameAsString()) {
      return true;
    }
    return false;
  }

  /// Clang AST context
  clang::ASTContext &context_;

  /// Entity class decl
  clang::CXXRecordDecl *object_decl_{nullptr};

  Analyzer *analyzer_{nullptr};
};

JSONで出力

こちらのJSONライブラリが使いやすかったのと、MIT LICENSEなので使わせて貰いました。

nlohmann/json: JSON for Modern C++

Visual Studio プロジェクトの Property > C/C++ > General > Additional Include Directoriesに追加。

$(ProjectDir)json/include

JSONで出力してみる。

bool Analyzer::GenerateJSON(const char* output_path) {
  nlohmann::json json;

  json["class"] = nlohmann::json::array();

  for (CXXRecord &record : records_) {
    // Nerver dump base class.
    if (record.qualified_name_ == base_class_) {
      continue;
    }

    nlohmann::json record_json;
    record_json["name"] = record.name_;
    record_json["qualified_name"] = record.qualified_name_;

    record_json["field"] = nlohmann::json::array();
    for (const Field &field : record.fields_) {
      nlohmann::json filed_json;
      filed_json["name"] = field.name_;
      filed_json["type"] = field.type_;
      record_json["field"].push_back(filed_json);
    }

    record_json["function"] = nlohmann::json::array();
    for (const Function &function : record.functions_) {
      nlohmann::json function_json;
      function_json["name"] = function.name_;
      record_json["function"].push_back(function_json);
    }

    json["class"].push_back(record_json);
  }

  std::ofstream steam(output_path);

  std::string json_dump = json.dump(2);
  steam << json_dump;

  return true;
}

出力されたJSON

{
  "class": [
    {
      "field": [
        {
          "name": "position_",
          "type": "std::array<float, 3>"
        }
      ],
      "function": [
        {
          "name": "GameObject"
        },
        {
          "name": "~GameObject"
        },
        {
          "name": "operator="
        },
        {
          "name": "position"
        },
        {
          "name": "set_position"
        }
      ],
      "name": "GameObject",
      "qualified_name": "melty::sample::GameObject"
    }
  ]
}

Sample

リポジトリ大きくなると嫌なので、LLVM Clang nlohmann jsonは含んでいません。独自に取得してプロジェクトと同じフォルダに配置してください。

samples/ClangAST at master · meltybk/samples

起動引数をこんな感じで与えると、構文解析してjsonを出力します。

D:/samples/ClangAST/sample.cc melty::sample::Object -ID:/samples/ClangAST/ -o D:/samples/ClangAST/object.json
タイトルとURLをコピーしました