编辑推荐: |
本文主要介绍了Clang AST的相关内容。希望对您的学习有所帮助 。
本文来自于CSDN,由火龙果软件Alice编辑、推荐。 |
|
前言
在之前搭建环境是就已经提到过,Clang 不仅仅可以作为一个编译器前端,同时还可以通过库的形式提供代码解析功能,将
C/C++ 程序源码转换为 abstract syntax tree (AST)语法树以及提供相应接口去操作
AST 语法树。参考资料
AST 结构基础
AST 中的每个节点都是 Decl 或 Stmt 类的一个实例:
Decl : 表示声明。Decl 下级还包含不同类型的子类用于标识不同的声明类型;
例如 FunctionDecl 类用于函数声明,ParmVarDecl 类用于函数参数声明。
Stmt : 表示语句(代码块)。同样存在Stmt的子类,对于不同的语句类型;
例如 IfStmt 用于标识 if 语句, ReturnStmt 类用于标识函数返回。
Example AST
先来一段演示代码:
#include <stdio.h>
int global;
void myPrint(int param) {
if (param == 1)
printf("param is 1");
for (int i = 0 ; i < 10 ; i++ ) {
global += i;
}
}
int main(int argc, char *argv[]) {
int param = 1;
myPrint(param);
return 0;
}
|
Decl
一个函数的根节点是一个 FunctionDecl 实例。
一个 FunctionDecl 可以通过一个 ParmVarDecl 来标识参数,注意 ParmVarDecl
与 FunctionDecl 是同级的,都属于 Decl 子类。
函数体是一个 Stmt 实例,其中函数体使用 CompoundStmt
来标识,同样的它也是 Stmt 的一个子类。
VarDecl 用于标识局部和全局变量的声明,注意如果变量声明时有个初始值,那么 VarDecl 就会有一个初始值的子节点。
FunctionDecl、ParmVarDecl 和 VarDecl 都有一个名称和一个声明类型,在遍历节点查找我们想要的代码块是非常好用的。
Stmt
Stmt 用于标识代码语句,包含的子类:
CompoundStmt类 用来标识代码块;
DeclStmt类 用来标识局部变量声明;
ReturnStmt类 标识函数返回。
Expr 作为 Stmt 的子类,用于标识表达式:
CallExpr 标识函数调用;
ImplicitCastExpr 用于标识隐式强转换的类型;
DeclRefExpr 标识引用声明的变量和函数;
IntegerLiteral 用于整型文字。
Stmt 可能包含一些有着附加信息的子节点,例如 CompoundStmt 标识在一个大括号中代码块的语句,其中的每个语句都是一个包含其他信息的子节点。
在包含附加信息的子节点中,例如
CallExpr 函数调用类,它的第一个子元素是函数指针,其他的子元素是函数参数,其他节点同理。
Expr类
会有一个表达式的类型,例如 CallExpr 中的节点有个 void 的类型。一些 Expr 的子类会包含一个值,例如
初始化的局部或全局变量 IntegerLiteral 子节点,就有一个 1 ‘int’ 。
现在让我们关注下更复杂一点的
myPrint 函数,可以看到在其函数体中包含了 IfStmt 和 ForStmt 两种 Stmt
子类。
IfStmt 有 4 中子节点:
可以看到一个奇怪的的条件变量(->NULL),这是因为 c++ 中可以在 if 语句的 condition
声明一个变量(而不是在 C 中);
做个实验,这样是不是就很清晰了。
接下来是一个条件判断节点;
然后是该 if 判断的代码段;
最后是 Else 的代码段。
ForStmt 有 5 个子节点:
for 循环判断的初始化语句,for(int i = 0; i < 10; i++);
VarDecl类标识的 for 的条件变量定义;
说的有点难懂,做个实验
for 判断条件,for(int i = 0; i < 10; i++);
++段,for(int i = 0; i < 10; i++);
Stmt 标识 for 中的循环代码块。
BinaryOperator 二元操作符,存在两个子节点; UnaryOperator 一元操作符,只有一个子节点。
遍历
Clang AST
通过官方的一篇教程以及下图 可以了解到构建、遍历 AST 树需要的几个功能类,分别是 CompilerInstance、FrontendAction、ParseAST、ASTConsumer、RecursiveASTVisitor。
CompilerInstance: 用于管理 Clang 编译器单个实例的 Helper 类。它主要有两个用处:
它管理运行编译器所需的各种对象,例如预处理器,目标信息和 AST 上下文。
它提供了用于构造和操作通用 Clang 对象的实用程序例程。
从 CompilerInstance 可以了解到两点用处: 第一点对我们构建 AST 用处不大,主要是第二点中是管理和操作
Clang Tool 工具实用的程序历程,这点很有启发。官方教程是通过 ASTFrontendActions
来实现 AST 树遍历。其中我们可以了解到:
在编写基于 Clang 的工具(例如 Clang 插件)或基于 LibTooling 的独立工具时,常见的入口点是
FrontendAction。其允许在编译过程中执行用户特定的操作。如果想要在 Clang AST
树上运行工具,提供了方便的接口 ASTFrontendAction,该接口负责执行操作。剩下的唯一部分是实现
CreateASTConsumer 方法,该方法为每个翻译单元返回 ASTConsumer。
从 FrontendAction 可以了解到如果想运行基于 libTooling 的工具,直接实现一个
ASTFrontendAction 入口以及 ASTConsumer 即可,这样完全没有 ParseAST
什么事情了呀??为了搞清楚我们还是一起来了解一下 clang::ParseAST() 它到底干了什么,这个方法提供了构建和遍历
AST 的功能,接口定义如下:
void ParseAST(Preprocessor &pp, ASTConsumer *C,
ASTContext &Ctx, bool PrintStats = false,
TranslationUnitKind TUKind = TU_Complete,
CodeCompleteConsumer *CompletionConsumer = nullptr,
bool SkipFunctionBodies = false);
|
其中注意 ASTConsumer *C ,根据注释描述可知解析完成的源码文件会通过 ASTConsumer
来回传给我们,所以在调用这个接口时我们要实现一个 ASTConsumer 用来获取、遍历 AST 树。查看
ASTConsumer 的定义可以发现许多回调接口,包括不同类型、层级 AST 结构,这下真相大白了,ParseAST()
接口可以说是 Clang AST 树解析和构建的核心了,但是它的特性其实主要在于解析以及通过钩子 ASTConsumer
来把分析后的 AST 节点回传给我们,而我们在使用 ASTFrontendAction 时是会重写
CreateASTConsumer 方法,相当于 ASTFrontendAction 会帮我们调用
ParseAST() 接口并将我们的 ASTConsumer 实例注册进去,这样我们在使用过程中完全不用关心它��,下边举一个编写自己
ASTConsumer 与 ASTFrontendAction 的例子,通过继承 ASTConsumer、ASTFrontendAction
然后重写相关函数接口即可:
virtual bool HandleTopLevelDecl(DeclGroupRef D);
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
StringRef InFile) override;
virtual void EndSourceFileAction() {}
#include <clang/AST/ASTConsumer.h>
#include <clang/Parse/ParseAST.h>
#include <clang/AST/DeclGroup.h>
#include <clang/Frontend/FrontendActions.h>
#include <clang/Rewrite/Core/Rewriter.h>
using namespace clang;
class MyASTConsumer : public ASTConsumer
{
public:
MyASTConsumer() {}
bool HandleTopLevelDecl(DeclGroupRef DR) override
{
for (auto& b : DR)
{
}
return true;
}
};
class MyFrontendAction : public ASTFrontendAction
{
public:
MyFrontendAction() = default;
void EndSourceFileAction() override
{
}
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance& CI, StringRef file) override
{
return std::make_unique<MyASTConsumer>();
}
};
|
ASTConsumer 中有很多 HOOK 函数,我这里以 HandleTopLevelDecl 接口为例,它会返回给我们
top-level 的节点,接下来就是遍历这个节点以下所有的信息。这里需要了解一个新的类模板 RecursiveASTVisitor,我们可以通过这个类模板生成一个自己的
visitor 用来遍历某个节点所有的子节点:
#include <clang/AST/ASTConsumer.h>
#include <clang/AST/DeclGroup.h>
#include <clang/AST/RecursiveASTVisitor.h>
#include <clang/Parse/ParseAST.h>
#include <clang/Rewrite/Core/Rewriter.h>
#include "spdlog/spdlog.h"
using namespace clang;
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor>
{
bool VisitStmt(Stmt* s)
{
spdlog::info("\t{} \n", s->getStmtClassName());
return true;
}
bool VisitFunctionDecl(FunctionDecl* f)
{
if (f->hasBody())
{
Stmt* FuncBody = f->getBody();
spdlog::info("{}\n", f->getName());
}
return true;
}
};
class MyASTConsumer : public ASTConsumer
{
public:
MyASTConsumer(Rewriter& R) {}
bool HandleTopLevelDecl(DeclGroupRef DR) override
{
for (auto& b : DR)
{
MyASTVisitor Visitor;
Visitor.TraverseDecl(b);
}
return true;
}
};
|
介绍下 RecursiveASTVisitor 类模板,它会按照深度优先的搜索顺序遍历每个 Stmt
节点,并且对 AST 树中的每个 Stmt 节点调用类模板中 VisitStmt() 方法,如果 VisitStmt
返回 false 的话,则递归遍历将结束。 最后还剩下一个疑问点就是,CompilerInstance
该怎么用起来呢?这里就要提到另一个类了 ClangTool,这个类可以让我们编写的功能模块像 clang-tidy、clang-format
等等,变身成命令行程序。大致来说就是将我们的 MyFrontendAction 传给它,会自动的创建
CompilerInstance 来运行,详细的使用方法在下边的例子里会给出。至此编写工具来解析 AST
树的方法介绍完毕了。
Example
接下来将通过上边的知识,分析下之前已经出场过的一个功能模块,一个遍历打印出AST信息,并且可以判断
if 分支的代码:
#include <clang/Frontend/FrontendActions.h>
#include <clang/Rewrite/Core/Rewriter.h>
namespace clang
{
class MyFrontendAction : public ASTFrontendAction
{
public:
MyFrontendAction() = default;
void EndSourceFileAction() override;
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance& CI, StringRef file) override;
private:
Rewriter TheRewriter;
};
}
int Function(int argc, const char** argv);
#include <sstream>
#include <string>
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/ASTConsumers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/raw_ostream.h"
#include "Function/LoopConvert.h"
using namespace clang;
using namespace clang::driver;
using namespace clang::tooling;
static llvm::cl::OptionCategory ToolingSampleCategory("Tooling Sample");
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor>
{
public:
MyASTVisitor(Rewriter& R) : TheRewriter(R) {}
bool VisitStmt(Stmt* s)
{
if (isa<IfStmt>(s))
{
auto* IfStatement = cast<IfStmt>(s);
Stmt* Then = IfStatement->getThen();
TheRewriter.InsertText(Then->getBeginLoc(), "// the 'if' part\n", true, true);
Stmt* Else = IfStatement->getElse();
if (Else)
TheRewriter.InsertText(Else->getBeginLoc(), "// the 'else' part\n", true, true);
}
return true;
}
bool VisitFunctionDecl(FunctionDecl* f)
{
if (f->hasBody())
{
Stmt* FuncBody = f->getBody();
QualType QT = f->getReturnType();
std::string TypeStr = QT.getAsString();
DeclarationName DeclName = f->getNameInfo().getName();
std::string FuncName = DeclName.getAsString();
std::stringstream SSBefore;
SSBefore << "// Begin function " << FuncName << " returning " << TypeStr << "\n";
SourceLocation ST = f->getSourceRange().getBegin();
TheRewriter.InsertText(ST, SSBefore.str(), true, true);
std::stringstream SSAfter;
SSAfter << "\n// End function " << FuncName;
ST = FuncBody->getEndLoc().getLocWithOffset(1);
TheRewriter.InsertText(ST, SSAfter.str(), true, true);
}
return true;
}
private:
Rewriter& TheRewriter;
};
class MyASTConsumer : public ASTConsumer
{
public:
MyASTConsumer(Rewriter& R) : Visitor(R) {}
bool HandleTopLevelDecl(DeclGroupRef DR) override
{
for (auto& b : DR)
{
Visitor.TraverseDecl(b);
b->dump();
}
return true;
}
private:
MyASTVisitor Visitor;
};
std::unique_ptr<ASTConsumer> MyFrontendAction::CreateASTConsumer(CompilerInstance& CI, StringRef file)
{
llvm::errs() << "** Creating AST consumer for: " << file << "\n";
TheRewriter.setSourceMgr(CI.getSourceManager(), CI.getLangOpts());
return std::make_unique<MyASTConsumer>(TheRewriter);
}
void MyFrontendAction::EndSourceFileAction()
{
SourceManager& SM = TheRewriter.getSourceMgr();
llvm::errs() << "** EndSourceFileAction for: " << SM.getFileEntryForID(SM.getMainFileID())->getName() << "\n";
TheRewriter.getEditBuffer(SM.getMainFileID()).write(llvm::outs());
}
int main(int argc, const char** argv)
{
CommonOptionsParser op(argc, argv, ToolingSampleCategory);
ClangTool Tool(op.getCompilations(), op.getSourcePathList());
return Tool.run(newFrontendActionFactory<MyFrontendAction>().get());
}
|
使用命令行来初始化 Clang 工具,这里需要使用 CommonOptionsParser 类。查看注释可以了解到,这个类是所有命令行
clang 工具公用的命令解析器,它可以解析命令行命令参数,例如指定 compilation commands
database 链接路径,或者用户执行时指定的其他指令。
class CommonOptionsParser {...}
|
ClangTool::run accepts a FrontendActionFactory 这也就是我们想要运行自己编写的
FrontendAction 入口了,将其传入即可。
当构建 AST 树后会调用 MyFrontendAction::CreateASTConsumer
来使用我们客制化实现的 ASTConsumer,并将相关节点返回给我们。MyFrontendAction
中可以发现有一个 TheRewriter 成员,这是一个重写器,主要是用来将我们 if else 添加完注释的代码进行回写。
HandleTopLevelDecl 会回调给我们相应的节点信息,使用 MyASTVisitor 来实现我们想要的功能即可。
其他注意
通过重写了 virtual bool HandleTopLevelDecl (DeclGroupRef
D) 来实现了遍历 top-level 的 Decl,这个接口有个特点是每次分析到一个顶层定义时就会回调,也就是说调用这个接口时文件还没有分析完成,相当于一边分析,一边调用,
DeclGroupRef 一组定义的列表节点引用。
还有一个 virtual void HandleTranslationUnit (ASTContext
&Ctx) 当整个翻译单元的 AST 已被解析时,将调用此方法。
ASTContext 包含在整个文件的语义分析中所查找到的长寿 AST 节点,例如类型以及定义。也就是说包含了文件分析后所有
AST 关键节点信息。
|