理解ClangAST

理解ClangAST

本文为译文,原文点击这里

clang无处不在;它是我最喜欢的Vim插件YouCompleteMe的核心,它最近得到了微软Visual Studio的支持,在CppCast的许多章节中都提到过它,它支持优秀的clang格式化器。当然,我想更好地理解clang前端如何在底层工作。

Clang前端和AST

Clang是LLVM的C语言家族前端。在编译器设计中,前端负责分析部分,这意味着根据语法结构将源代码分成若干块。结果是一个中间表示,它被后端转换到目标程序中,称为合成。可选地,有一个在前端和后端之间的中间步骤称为优化。
图片来自http://www.aosabook.org/en/llvm.html

图片来自http://www.aosabook.org/en/llvm.html
具体来说,前端负责解析源代码、检查错误并将输入代码转换为抽象语法树(AST)。AST是一种结构化表示,可以用于不同的目的,如创建符号表、执行类型检查和最后生成代码。AST是我主要感兴趣的部分,因为它是clang的核心,所有有趣的事情都在这里发生。 在clang文档中,我找到了下面的视频。Manuel Klimek给出了AST及其应用的简单解释。我强烈推荐大家看这个视频!

需科学上网

ASTContext

ASTContext保存关于翻译单元的AST的信息,这些信息并不存储在它的节点中。例如,标识符表和源管理器等等。它还通过TranslationUnitDecl* getTranslationUnitDecl()方法形成AST的入口点。

AST使用三组核心类构建:声明(delclarations)、语句(statements)和类型(types)。如果您跟随到doxygen的链接,您将看到这三个类构成了一系列专门化的基础。但是,需要注意的是它们不是从公共基类继承的。因此,没有访问树中所有节点的公共接口。每个节点都有专门的遍历方法,允许您导航树。接下来,我将展示如何在不需要公共API的情况下高效地使用访问者。

例子

考虑下面代码示例中的if语句,它由下面打印的AST中的IfStmt表示。它由一个条件Expr (BinaryOperator)和两个stmt组成,一个用于then-case,另一个用于else-case。

$ cat example.cpp
int f(int i) {
    if (i > 0) {
        return true;
    } else {
        return false;
    }
}

$ clang -Xclang -ast-dump -fsyntax-only example.cpp
[part of the AST left out for conciseness]
    `-IfStmt 0x2ac4638 <line:2:5, line:6:5>
      |-<<<NULL>>>
      |-BinaryOperator 0x2ac4540 <line:2:9, col:13> '_Bool' '>'
      | |-ImplicitCastExpr 0x2ac4528 <col:9> 'int' <LValueToRValue>
      | | `-DeclRefExpr 0x2ac44e0 <col:9> 'int' lvalue ParmVar 0x2ac4328 'i' 'int'
      | `-IntegerLiteral 0x2ac4508 <col:13> 'int' 0
      |-CompoundStmt 0x2ac45b0 <col:16, line:4:5>
      | `-ReturnStmt 0x2ac4598 <line:3:9, col:16>
      |   `-ImplicitCastExpr 0x2ac4580 <col:16> 'int' <IntegralCast>
      |     `-CXXBoolLiteralExpr 0x2ac4568 <col:16> '_Bool' true
      `-CompoundStmt 0x2ac4618 <line:4:12, line:6:5>
        `-ReturnStmt 0x2ac4600 <line:5:9, col:16>
          `-ImplicitCastExpr 0x2ac45e8 <col:16> 'int' <IntegralCast>
            `-CXXBoolLiteralExpr 0x2ac45d0 <col:16> '_Bool' false

如何遍历子树呢?很简单,只需调用IfStmt的专用方法之一。

const Expr * getCond () const
const Stmt * getThen () const
const Stmt * getElse () const

树中的每个节点都是具有专用方法的特定类。如上所示,转储AST是找到合适的类表示的最简单方法。找到正确的方法只是查看类引用的问题。

操纵源码

令牌的位置由SourceLocation类表示。由于此对象嵌入到许多AST节点中,因此需要非常小。这是通过与SourceManager一起对源文件中的实际位置进行编码来实现的。

AST遍历

为了操作AST,我们需要能够遍历语法树。Clang提供了两个抽象来简化我们的工作:RecursiveASTVisitor
ASTMatchers

让我们考虑3种可用的方法来处理基本用例,其中我们希望在给定的源文件test.cpp中找到对doSomething的所有调用。

前两个工具使用LibTooling,这是一个允许创建独立工具的库。除了提供良好的c++接口之外,它还负责构建实际的语法树,从而避免了重复劳动。您可以完全控制AST,甚至可以使用Clang插件共享代码。但是,由于LLVM和Clang是移动迅速的项目,所以在发布新版本时可能需要更改一些API调用。

然而,当高级和稳定的接口很重要时,Clang的C接口(LibClang)是首选。在第三个示例中,我们研究了带有游标的AST遍历,游标表示抽象语法树中的位置。

递归ASTVisitor

递归AST访问器允许您以深度优先的方式遍历Clang AST的节点。我们通过扩展类和实现所需的visit*方法来访问特定的节点,例如下面的例子中的VisitCallExpr。有关如何构建自己的访问者的逐步教程,请参阅Clang的文档中的本教程。

#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Driver/Options.h"
#include "clang/Frontend/ASTConsumers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"

using namespace clang;
using namespace clang::tooling;

class FindNamedCallVisitor : public RecursiveASTVisitor<FindNamedCallVisitor> {
 public:
  explicit FindNamedCallVisitor(ASTContext *Context, std::string fName)
      : Context(Context), fName(fName) {}

  bool VisitCallExpr(CallExpr *CallExpression) {
    QualType q = CallExpression->getType();
    const Type *t = q.getTypePtrOrNull();

    if (t != NULL) {
      FunctionDecl *func = CallExpression->getDirectCallee();
      const std::string funcName = func->getNameInfo().getAsString();
      if (fName == funcName) {
        FullSourceLoc FullLocation =
            Context->getFullLoc(CallExpression->getLocStart());
        if (FullLocation.isValid())
          llvm::outs() << "Found call at "
                       << FullLocation.getSpellingLineNumber() << ":"
                       << FullLocation.getSpellingColumnNumber() << "\n";
      }
    }

    return true;
  }

 private:
  ASTContext *Context;
  std::string fName;
};

class FindNamedCallConsumer : public clang::ASTConsumer {
 public:
  explicit FindNamedCallConsumer(ASTContext *Context, std::string fName)
      : Visitor(Context, fName) {}

  virtual void HandleTranslationUnit(clang::ASTContext &Context) {
    Visitor.TraverseDecl(Context.getTranslationUnitDecl());
  }

 private:
  FindNamedCallVisitor Visitor;
};

class FindNamedCallAction : public clang::ASTFrontendAction {
 public:
  FindNamedCallAction() {}

  virtual std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
      clang::CompilerInstance &Compiler, llvm::StringRef InFile) {
    const std::string fName = "doSomething";
    return std::unique_ptr<clang::ASTConsumer>(
        new FindNamedCallConsumer(&Compiler.getASTContext(), fName));
  }
};

static llvm::cl::OptionCategory MyToolCategory("my-tool options");

int main(int argc, const char **argv) {
  const std::string fName = "doSomething";

  CommonOptionsParser OptionsParser(argc, argv, MyToolCategory);
  ClangTool Tool(OptionsParser.getCompilations(),
                 OptionsParser.getSourcePathList());

  // run the Clang Tool, creating a new FrontendAction (explained below)
  int result = Tool.run(newFrontendActionFactory<FindNamedCallAction>().get());
  return result;
}

AST Matchers

AST Matcher API提供了一种域特定语言(DSL)来匹配Clang的AST上的谓词。

callExpr(callee(functionDecl(hasName("doSomething"))))

AST Matcher引用解释了如何使用DSL为感兴趣的节点构建匹配器。附加一个MatchCallback,它在谓词匹配时执行。LLVM文档包含了关于匹配器入门的全面教程。

#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"

#include "llvm/Support/CommandLine.h"

using namespace clang::ast_matchers;
using namespace clang::tooling;
using namespace clang;
using namespace llvm;

class MyPrinter : public MatchFinder::MatchCallback {
 public:
  virtual void run(const MatchFinder::MatchResult &Result) {
    ASTContext *Context = Result.Context;
    if (const CallExpr *E =
            Result.Nodes.getNodeAs<clang::CallExpr>("functions")) {
      FullSourceLoc FullLocation = Context->getFullLoc(E->getLocStart());
      if (FullLocation.isValid()) {
        llvm::outs() << "Found call at " << FullLocation.getSpellingLineNumber()
                     << ":" << FullLocation.getSpellingColumnNumber() << "\n";
      }
    }
  }
};

// Apply a custom category to all command-line options so that they are the
// only ones displayed.
static llvm::cl::OptionCategory MyToolCategory("my-tool options");

// CommonOptionsParser declares HelpMessage with a description of the common
// command-line options related to the compilation database and input files.
// It's nice to have this help message in all tools.
static cl::extrahelp CommonHelp(CommonOptionsParser::HelpMessage);

// A help message for this specific tool can be added afterwards.
static cl::extrahelp MoreHelp("\nMore help text...");

int main(int argc, const char **argv) {
  CommonOptionsParser OptionsParser(argc, argv, MyToolCategory);
  ClangTool Tool(OptionsParser.getCompilations(),
                 OptionsParser.getSourcePathList());

  MyPrinter Printer;
  MatchFinder Finder;

  StatementMatcher functionMatcher =
      callExpr(callee(functionDecl(hasName("doSomething")))).bind("functions");

  Finder.addMatcher(functionMatcher, &Printer);

  return Tool.run(newFrontendActionFactory(&Finder).get());
}

在这两种情况下,我们都使用ASTContext来获取语句在源文件中的位置。注意,对于AST匹配器,上下文信息是提供的,而我们需要在访问者方法中处理这个问题。

这两个程序的输出是相同的。双破折号表示不使用编译数据库,而是将(no)编译器选项作为参数传递。

$ cat test.cpp
class Y {
public: void doSomething();
};

void z() { Y y; y.doSomething(); }

int doSomething(int i) {
    if (i == 0) return 0;
    return 1 + doSomething(i--);
}

int main() {
    return doSomething(2);
}

$ bin/examle test.cpp --
Found call at 5:17
Found call at 9:16
Found call at 13:12

Cursors

第三个也是最后一个例子使用了Clang的C库LibClang。这种方法的核心是visit回调函数,但是我们需要先处理一些样板文件。我们首先创建一个表示一组翻译单元的索引。接下来,我们通过解析一个给定的文件来构建一个翻译单元,这里的文件指定为第一个命令行参数argv[1]。解析函数接受一个命令行参数数组的两个参数,这些参数可能是编译文件所必需的,例如包含路径或sysroot的规范。参数nullptr和0表示内存中没有(0)未保存的文件。最后一个参数告诉解析器跳过函数体以加快解析速度。

一旦我们有了翻译单元,我们就可以使用访问者回调函数访问AST中的每个子元素。使用游标参数,我们可以检查所访问节点的类型,并提取其源位置的信息。我们可以忽略其他两个参数,因为我们只需要考虑当前节点。

#include <clang-c/Index.h>

#include <iostream>

CXChildVisitResult visitor(CXCursor cursor, CXCursor, CXClientData) {
  CXCursorKind kind = clang_getCursorKind(cursor);

  // Consider functions and methods
  if (kind == CXCursorKind::CXCursor_FunctionDecl ||
      kind == CXCursorKind::CXCursor_CXXMethod) {
    auto cursorName = clang_getCursorDisplayName(cursor);

    // Print if function/method starts with doSomething
    auto cursorNameStr = std::string(clang_getCString(cursorName));
    if (cursorNameStr.find("doSomething") == 0) {
      // Get the source locatino
      CXSourceRange range = clang_getCursorExtent(cursor);
      CXSourceLocation location = clang_getRangeStart(range);

      CXFile file;
      unsigned line;
      unsigned column;
      clang_getFileLocation(location, &file, &line, &column, nullptr);

      auto fileName = clang_getFileName(file);

      std::cout << "Found call to " << clang_getCString(cursorName) << " at "
                << line << ":" << column << " in " << clang_getCString(fileName)
                << std::endl;

      clang_disposeString(fileName);
    }

    clang_disposeString(cursorName);
  }

  return CXChildVisit_Recurse;
}

int main(int argc, char **argv) {
  if (argc < 2) {
    return 1;
  }

  // Command line arguments required for parsing the TU
  constexpr const char *ARGUMENTS[] = {};

  // Create an index with excludeDeclsFromPCH = 1, displayDiagnostics = 0
  CXIndex index = clang_createIndex(1, 0);

  // Speed up parsing by skipping function bodies
  CXTranslationUnit translationUnit = clang_parseTranslationUnit(
      index, argv[1], ARGUMENTS, std::extent<decltype(ARGUMENTS)>::value,
      nullptr, 0, CXTranslationUnit_SkipFunctionBodies);

  // Visit all the nodes in the AST
  CXCursor cursor = clang_getTranslationUnitCursor(translationUnit);
  clang_visitChildren(cursor, visitor, 0);

  // Release memory
  clang_disposeTranslationUnit(translationUnit);
  clang_disposeIndex(index);

  return 0;
}

上面的代码说明了Clang的C接口使AST的遍历变得非常简单。高级构造及其稳定的接口使libclang成为许多与Clang交互的第三方应用程序的首选方法。

一个例子

要构建前两个示例,您有两个选项。要么构建“树内”(使用CMake),要么单独编译每个文件并链接到Clang & LLVM。我假设您从源代码构建了LLVM,并在/opt/ LLVM下安装了它。

前一种方法是我最初开始写这篇文章时的方法。在如何创建clang工具的教程中有很好的文档说明。

另外,如果安装了Clang & LLVM,则可以分别编译每个示例。使用llvm-config指定链接到LLVM的标志和对象库。这并不包括clang库,所以不要忘记显式地指定它们。

clang++ example.cpp 
	$(/opt/llvm/bin/llvm-config --cxxflags) \ 
	$(/opt/llvm/bin/llvm-config --ldflags --libs --system-libs) \
	-lclangAST \
	-lclangASTMatchers \
	-lclangAnalysis \
	-lclangBasic \
	-lclangDriver \
	-lclangEdit \
	-lclangFrontend \
	-lclangFrontendTool \
	-lclangLex \
	-lclangParse \
	-lclangSema \
	-lclangEdit \
	-lclangRewrite \
	-lclangRewriteFrontend \
	-lclangStaticAnalyzerFrontend \
	-lclangStaticAnalyzerCheckers \
	-lclangStaticAnalyzerCore \
	-lclangSerialization \
	-lclangToolingCore \
	-lclangTooling \
	-lclangFormat 

编译第三个示例更加简单。因为LibClang是一个小型的Clang C API,所以不需要链接LLVM。所有你需要编译是下面的命令:

clang++ example.cpp -std=c++11 -g -I/opt/llvm/include -L/opt/llvm/lib -lclang

总结

当我开始写这篇博文的时候,我对clang的内部原理几乎一无所知。作为LLVM的一部分,它看起来像一个庞大而令人生畏的项目。然而,由于一些优秀的文档的可用性,它非常容易理解基本概念。我希望这三个示例可以为任何希望深入编写基于clang的工具的人提供一个起点。

衷心感谢所有为LLVM项目及其文档做出贡献的人!

相关的

最近一期的CppCast提到了使用Clang进行c++静态分析,其中详细介绍了如何使用Clang构建静态分析器。

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值