理解ClangAST
本文为译文,原文点击这里
clang无处不在;它是我最喜欢的Vim插件YouCompleteMe的核心,它最近得到了微软Visual Studio的支持,在CppCast的许多章节中都提到过它,它支持优秀的clang格式化器。当然,我想更好地理解clang前端如何在底层工作。
Clang前端和AST
Clang是LLVM的C语言家族前端。在编译器设计中,前端负责分析部分,这意味着根据语法结构将源代码分成若干块。结果是一个中间表示,它被后端转换到目标程序中,称为合成。可选地,有一个在前端和后端之间的中间步骤称为优化。
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构建静态分析器。