使用 LibTooling 和 LibASTMatchers 构建工具的教程
本文为译文,点击 此处查看原文。
本文档旨在展示如何基于 Clang 的 LibTooling 构建一个有用的源代码到源代码的转换工具。它明确地针对初次使用 Clang 的人,所以您所需要的只是 C++ 和命令行方面的工作知识。
为了使用编译器,您需要一些抽象语法树(AST)的基本知识。为了达到这个目的,建议读者略读一下 Clang AST的介绍。
1. 步骤0:获得 Clang
由于 Clang 是 LLVM 项目的一部分,您需要首先下载LLVM的源代码。Clang和LLVM都在同一个 git 存储库中,在不同的目录下。有关更多信息,请参见LLVM系统入门指南。
cd ~/clang-llvm
git clone https://github.com/llvm/llvm-project.git
接下来,您需要获得CMake
构建系统和Ninja
构建工具。
cd ~/clang-llvm
git clone https://github.com/martine/ninja.git
cd ninja
git checkout release
./bootstrap.py
sudo cp ninja /usr/bin/
cd ~/clang-llvm
git clone git://cmake.org/stage/cmake.git
cd cmake
git checkout next
./bootstrap
make
sudo make install
好吧。现在我们要构建 Clang:
cd ~/clang-llvm
mkdir build && cd build
cmake -G Ninja ../llvm -DLLVM_ENABLE_PROJECTS=clang -DLLVM_BUILD_TESTS=ON # Enable tests; default is off.
ninja
ninja check # Test LLVM only.
ninja clang-test # Test Clang only.
ninja install
我们构建完成了,所有的测试都应该通过。
最后,我们希望将 Clang 设置为它自己的编译器。
cd ~/clang-llvm/build
ccmake ../llvm
第二个命令将显示用于配置 Clang 的一个 GUI。您需要为CMAKE_CXX_COMPILER
设置条目。按“t”
键打开高级模式。向下滚动到CMAKE_CXX_COMPILER
,并将其设置为/usr/bin/clang++
,或您安装它的任何位置。按“c”
进行配置,然后按“g”
生成CMake
的文件。
最后,最后一次运行ninja
,你就完成了。
2. 步骤1:创建一个 ClangTool
现在我们已经有了足够的背景知识,是时候创建现有的最简单的有效ClangTool
了:语法检查器(syntax checker)。虽然这已经作为clang-check
存在,但重要的是要了解发生了什么。
首先,我们需要为我们的工具创建一个新目录,并告诉CMake
它存在。由于这不是一个核心 clang 工具,它将驻留在tools/extra
存储库中。
cd ~/clang-llvm/llvm/tools/clang
mkdir tools/extra/loop-convert
echo 'add_subdirectory(loop-convert)' >> tools/extra/CMakeLists.txt
vim tools/extra/loop-convert/CMakeLists.txt
CMakeLists.txt
应该包含以下内容:
set(LLVM_LINK_COMPONENTS support)
add_clang_executable(loop-convert
LoopConvert.cpp
)
target_link_libraries(loop-convert
PRIVATE
clangTooling
clangBasic
clangASTMatchers
)
这样,Ninja
就可以编译我们的工具了。让我们给它一些编译的东西!将以下内容放入tools/extra/loop-convert/LoopConvert.cpp
中。关于为什么需要不同的部分的详细说明可以在 LibTooling文档 中找到。
// Declares clang::SyntaxOnlyAction.
#include "clang/Frontend/FrontendActions.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
// Declares llvm::cl::extrahelp.
#include "llvm/Support/CommandLine.h"
using namespace clang::tooling;
using namespace llvm;
// 将自定义类别应用于所有命令行选项,以便仅显示它们。
static cl::OptionCategory MyToolCategory("my-tool options");
// CommonOptionsParser使用与编译数据库和输入文件相关的公共命令行选项的描述声明 HelpMessage。
// 在所有工具中都有这个帮助消息,这很好。
static cl::extrahelp CommonHelp(CommonOptionsParser::HelpMessage);
// 稍后可以添加此特定工具的帮助消息。
static cl::extrahelp MoreHelp("\nMore help text...\n");
int main(int argc, const char **argv) {
CommonOptionsParser OptionsParser(argc, argv, MyToolCategory);
ClangTool Tool(OptionsParser.getCompilations(),
OptionsParser.getSourcePathList());
return Tool.run(newFrontendActionFactory<clang::SyntaxOnlyAction>().get());
}
这是它!您可以从build目录中运行ninja
来编译我们的新工具。
cd ~/clang-llvm/build
ninja
现在,您应该能够在任何源文件上运行位于~/clang-llvm/build/bin
中的语法检查器。试一试!
echo "int main() { return 0; }" > test.cpp
bin/loop-convert test.cpp --
请注意在指定源文件之后的两个破折号。编译器的附加选项是在破折号之后传递的,而不是从编译数据库加载它们 —— 只是现在不需要任何选项。
3. Intermezzo:学习 AST matcher 基础知识
Clang最近引入了ASTMatcher库,以提供一种简单、强大和简洁的方式来描述 AST 中的特定模式。作为由宏和模板支持的一个DSL实现(如果您感兴趣,请参阅 ASTMatchers.h),matchers
提供了函数式编程语言中常见的代数数据类型。
例如,假设您只想检查二进制操作符。有一个matcher
可以做到这一点,它被方便地命名为binaryOperator
。你可以猜一下这个matcher
是做什么的:
binaryOperator(hasOperatorName("+"), hasLHS(integerLiteral(equals(0))))
令人震惊的是,它将与加法表达式
匹配,而加法表达式的左边恰好是文字0
。它不会与其他形式的0
匹配,比如'\0'
或NULL
,但是它会与扩展到0
的宏匹配。matcher
也不会匹配对重载操作符“+”
的调用,因为有一个单独的操作符operatorCallExpr
matcher来处理重载操作符。
有一些AST matchers
来匹配 AST 的所有不同节点,将matchers
缩小到只匹配满足特定条件的 AST 节点,并遍历matchers
来得到,从一种AST节点到另一种AST节点。有关 AST matchers 的完整列表,请查看 AST Matcher References。
所有的matcher
都是名词,它们描述 AST 中的实体,并且可以被绑定,以便在找到一个匹配时引用它们。要做到这一点,只需调用这些匹配器上的bind
方法,例如:
variable(hasType(isInteger())).bind("intvar")
4. 步骤2:使用 AST matchers
好了,说到真正使用matchers
。我们首先定义一个matcher
,它将捕获所有定义一个初始化为零的新变量的for
语句。让我们从匹配所有for
循环开始:
forStmt()
接下来,我们要指定在循环的第一部分中声明一个变量,这样就可以将matcher
扩展到:
forStmt(hasLoopInit(declStmt(hasSingleDecl(varDecl()))))
最后,我们可以添加变量初始化为零的条件(condition)。
forStmt(hasLoopInit(declStmt(hasSingleDecl(varDecl(
hasInitializer(integerLiteral(equals(0))))))))
读取和理解matcher
定义相当容易(“匹配这样的循环,它们的初始化部分声明了一个初始化为整型文字0的变量”),但是确定每个部分都是必需的则更加困难。注意,这个matcher
不会匹配变量初始化为'\0'
、0.0
、NULL
或除整数0之外的任何形式的0的循环。
最后一步是给matcher
一个名称,并绑定ForStmt
,因为我们将用它做一些事情:
StatementMatcher LoopMatcher =
forStmt(hasLoopInit(declStmt(hasSingleDecl(varDecl(
hasInitializer(integerLiteral(equals(0)))))))).bind("forLoop");
一旦定义了matcher
,就需要添加更多的脚手架(scaffolding)来运行它们。matcher
与一个MatchCallback
配对,并在一个MatchFinder
对象中注册,然后从一个ClangTool
运行。更多的代码!
将以下内容添加到LoopConvert.cpp
:
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
using namespace clang;
using namespace clang::ast_matchers;
StatementMatcher LoopMatcher =
forStmt(hasLoopInit(declStmt(hasSingleDecl(varDecl(
hasInitializer(integerLiteral(equals(0)))))))).bind("forLoop");
class LoopPrinter : public MatchFinder::MatchCallback {
public :
virtual void run(const MatchFinder::MatchResult &Result) {
if (const ForStmt *FS = Result.Nodes.getNodeAs<clang::ForStmt>("forLoop"))
FS->dump();
}
};
并将main()改为:
int main(int argc, const char **argv) {
CommonOptionsParser OptionsParser(argc, argv, MyToolCategory);
ClangTool Tool(OptionsParser.getCompilations(),
OptionsParser.getSourcePathList());
LoopPrinter Printer;
MatchFinder Finder;
Finder.addMatcher(LoopMatcher, &Printer);
return Tool.run(newFrontendActionFactory(&Finder).get());
}
现在,您应该能够重新编译并运行代码来发现for
循环。创建一个新文件与几个例子,并测试我们的新代码:
cd ~/clang-llvm/llvm/llvm_build/
ninja loop-convert
vim ~/test-files/simple-loops.cc
bin/loop-convert ~/test-files/simple-loops.cc
5. 步骤3.5:更复杂的 Matchers
我们的简单matcher
能够发现for
循环,但是我们仍然需要过滤掉更多的循环。我们可以用一些精心选择的matchers
来完成剩下的工作,但是首先我们需要确定我们想要允许哪些属性。
我们如何描述数组上的for
循环,哪些循环适合转换为基于范围(range-based)的语法?基于范围的循环在大小为N的数组上,其中:
- (1) 从索引
0
开始 - (2) 连续迭代
- (3) 以索引
N-1
结束
我们已经检查了(1),所以我们只需要对循环的条件添加一个检查,以确保循环的索引变量与N进行比较,然后再进行一个检查,以确保增量步骤只是对相同的变量进行增量。用于(2)的matcher
很简单:需要在初始化部分中声明的相同变量的前置或后置增量。
不幸的是,不可能编写这样的matcher
。matcher
不包含用于比较任意两个AST节点并确定它们是否相等的逻辑,因此我们能做的最好的事情就是匹配超出我们所允许的范围,并对回调函数进行额外的比较。
无论如何,我们可以开始构建这个sub-matcher
。我们可以要求增量步骤是这样的一元增量:
hasIncrement(unaryOperator(hasOperatorName("++")))
指定递增的内容引入了Clang AST
的另一个特性:变量的用法表示为DeclRefExpr
(“declaration reference expressions,声明引用表达式”),因为它们是引用变量声明的表达式。要找到一个引用特定声明的unaryOperator
,我们只需添加第二个条件:
hasIncrement(unaryOperator(
hasOperatorName("++"),
hasUnaryOperand(declRefExpr())))
此外,我们可以限制我们的matcher
,只有当增加的变量是整数:
hasIncrement(unaryOperator(
hasOperatorName("++"),
hasUnaryOperand(declRefExpr(to(varDecl(hasType(isInteger())))))))
最后一步是将一个标识符附加到这个变量上,这样我们就可以在回调函数中检索它:
hasIncrement(unaryOperator(
hasOperatorName("++"),
hasUnaryOperand(declRefExpr(to(
varDecl(hasType(isInteger())).bind("incrementVariable"))))))
我们可以将这段代码添加到LoopMatcher
的定义中,并确保我们的程序(配备了新的matcher
)只输出这样的循环,这些循环声明一个初始化为零的变量,并且具有一个由某个变量的一元增量组成的增量步骤。
现在,我们只需要添加一个matcher
来检查for
循环的condition
部分是否将一个变量与该数组的大小进行比较。这里只有一个问题 —— 如果不查看循环体,我们就不知道要遍历哪个数组!我们再次被限制使用matchers
逼近我们想要的结果,在回调函数中填充细节。我们从这里开始:
hasCondition(binaryOperator(hasOperatorName("<"))
确保左边是对一个变量的引用,并且右边是integer
类型,这是有意义的。
hasCondition(binaryOperator(
hasOperatorName("<"),
hasLHS(declRefExpr(to(varDecl(hasType(isInteger()))))),
hasRHS(expr(hasType(isInteger())))))
为什么?因为它不起作用。test-files/simple.cpp
中提供的三个循环,都没有一个匹配条件。快速查看第一个for
循环的AST转储,由之前的loop-convert
迭代生成,给出了答案:
(ForStmt 0x173b240
(DeclStmt 0x173afc8
0x173af50 "int i =
(IntegerLiteral 0x173afa8 'int' 0)")
<<>>
(BinaryOperator 0x173b060 '_Bool' '<'
(ImplicitCastExpr 0x173b030 'int'
(DeclRefExpr 0x173afe0 'int' lvalue Var 0x173af50 'i' 'int'))
(ImplicitCastExpr 0x173b048 'int'
(DeclRefExpr 0x173b008 'const int' lvalue Var 0x170fa80 'N' 'const int')))
(UnaryOperator 0x173b0b0 'int' lvalue prefix '++'
(DeclRefExpr 0x173b088 'int' lvalue Var 0x173af50 'i' 'int'))
(CompoundStatement ...
我们已经知道声明和增量都匹配,否则这个循环不会被转储。罪魁祸首在于应用于小于(less-than)操作符的第一个操作数(即LHS)的隐式强制转换,即应用于引用 i
的表达式的L-value
到R-value
的转换。谢天谢地,matcher
库以ignoringParenImpCasts
的形式提供了一个解决这个问题的方法,指示matcher
在继续匹配之前忽略隐式转换和括号赛。调整condition操作符将还原所需的匹配。
hasCondition(binaryOperator(
hasOperatorName("<"),
hasLHS(ignoringParenImpCasts(declRefExpr(
to(varDecl(hasType(isInteger())))))),
hasRHS(expr(hasType(isInteger())))))
在向希望捕获的表达式添加绑定并将标识符字符串提取为变量之后,我们已经完成了array-step-2
。
6. 步骤4:检索匹配的节点
到目前为止,matcher
回调函数还不是很有趣:它只是转储循环的AST。在某个时候,我们将需要对输入源代码进行更改。接下来,我们将使用上一步中绑定的节点。
回调函数MatchFinder::run()
以MatchFinder::MatchResult&
作为参数。我们最感兴趣的是它的Context
和Nodes
成员。正如名称所示,Clang使用ASTContext
类来表示AST
的上下文信息,尽管功能上最重要的细节是一些操作需要一个ASTContext*
参数。更直接有用的是一组匹配的节点,以及我们如何检索它们。
由于绑定了三个变量(由ConditionVarName
、InitVarName
和IncrementVarName
标识),所以可以使用getNodeAs()
成员函数获得匹配的节点。
在LoopConvert.cpp
添加
#include "clang/AST/ASTContext.h"
更改LoopMatcher
为:
StatementMatcher LoopMatcher =
forStmt(hasLoopInit(declStmt(
hasSingleDecl(varDecl(hasInitializer(integerLiteral(equals(0))))
.bind("initVarName")))),
hasIncrement(unaryOperator(
hasOperatorName("++"),
hasUnaryOperand(declRefExpr(
to(varDecl(hasType(isInteger())).bind("incVarName")))))),
hasCondition(binaryOperator(
hasOperatorName("<"),
hasLHS(ignoringParenImpCasts(declRefExpr(
to(varDecl(hasType(isInteger())).bind("condVarName"))))),
hasRHS(expr(hasType(isInteger())))))).bind("forLoop");
并更改LoopPrinter::run
为:
void LoopPrinter::run(const MatchFinder::MatchResult &Result) {
ASTContext *Context = Result.Context;
const ForStmt *FS = Result.Nodes.getNodeAs<ForStmt>("forLoop");
// We do not want to convert header files!
if (!FS || !Context->getSourceManager().isWrittenInMainFile(FS->getForLoc()))
return;
const VarDecl *IncVar = Result.Nodes.getNodeAs<VarDecl>("incVarName");
const VarDecl *CondVar = Result.Nodes.getNodeAs<VarDecl>("condVarName");
const VarDecl *InitVar = Result.Nodes.getNodeAs<VarDecl>("initVarName");
if (!areSameVariable(IncVar, CondVar) || !areSameVariable(IncVar, InitVar))
return;
llvm::outs() << "Potential array-based loop discovered.\n";
}
Clang将VarDecl
与每个变量关联起来,以表示变量的声明。由于每个声明的“规范”形式按地址是唯一的,所以我们需要做的就是确保ValueDecl
(VarDecl
的基类)都不是NULL
,并比较规范的Decls
。
static bool areSameVariable(const ValueDecl *First, const ValueDecl *Second) {
return First && Second &&
First->getCanonicalDecl() == Second->getCanonicalDecl();
}
如果执行到达LoopPrinter::run()
的末尾,我们知道循环shell看起来是这样的:
for (int i= 0; i < expr(); ++i) { ... }
现在,我们只打印一条消息解释我们发现了一个循环。下一节将处理递归遍历 AST 以发现所需的所有更改。
顺便提一下,测试两个表达式是否相同并不简单,不过Clang已经为我们做了大量的工作,它提供了一种将表达式规范化
的方法:
static bool areSameExpr(ASTContext *Context, const Expr *First,
const Expr *Second) {
if (!First || !Second)
return false;
llvm::FoldingSetNodeID FirstID, SecondID;
First->Profile(FirstID, *Context, true);
Second->Profile(SecondID, *Context, true);
return FirstID == SecondID;
}
这段代码依赖于两个llvm::FoldingSetNodeIDs
之间的比较。正如Stmt::Profile()
的文档所指出的,Profile()
成员函数根据 AST 及其子节点的属性构建对 AST 中节点的描述。然后FoldingSetNodeID
作为一个hash,我们可以使用它来比较表达式。我们以后需要areSameExpr
。在您对添加到test-files/simple.cpp
的额外循环运行新代码之前,试着找出哪些可能被认为是可转换的。