使用LibTooling和LibASTMatchers构建工具的教程
本文档旨在说明如何基于Clang的LibTooling构建有用的源到源翻译工具。它专门针对刚接触Clang的人员,因此您所需要的只是对C ++和命令行的了解。
为了在编译器上工作,您需要一些抽象语法树(AST)的基础知识。为此,鼓励读者略读Clang AST简介
步骤0:获取clang
由于Clang是LLVM项目的一部分,因此您需要首先下载LLVM的源代码。Clang和LLVM都在同一个git仓库中,位于不同目录下。有关更多信息,请参见《入门指南》。
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;clang-tools-extra" -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的文件。
最后,最后一次运行忍者,就完成了。
第1步:创建一个ClangTool
现在我们已经掌握了足够的背景知识,是时候创建现有的最简单的高效ClangTool了:语法检查器。尽管该名称已经存在clang-check
,但是了解正在发生的事情很重要。
首先,我们需要为我们的工具创建一个新目录,并告诉CMake它已经存在。由于它将不再是核心的clang工具,因此它将存在于clang-tools-extra
存储库中。
cd ~/clang-llvm
mkdir clang-tools-extra/loop-convert
echo 'add_subdirectory(loop-convert)' >> clang-tools-extra/CMakeLists.txt
vim clang-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将能够编译我们的工具。让我们给它一些编译的东西!将以下内容放入 clang-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;
// 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...\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 --
请注意我们指定源文件后的两个破折号。破折号后会传递编译器的其他选项,而不是从编译数据库中加载它们-现在不需要任何选项。
Intermezzo:了解AST匹配器基础知识
Clang最近引入了ASTMatcher库,以提供一种简单,强大且简洁的方式来描述AST中的特定模式。匹配器实现为由宏和模板提供支持的DSL(如果您感到好奇,请参阅 ASTMatchers.h),匹配器可提供功能编程语言所通用的代数数据类型。
例如,假设您只想检查二进制运算符。有一个匹配器可以精确地将其命名为binaryOperator
。我给你一个猜猜这个匹配器的作用:
binaryOperator(hasOperatorName("+"), hasLHS(integerLiteral(equals(0))))
令人震惊的是,它将与左侧恰好是文字0的加法表达式匹配。它将与其他形式的0(例如'\0'
或)不匹配NULL
,但将与扩展为0的宏匹配。匹配器也将与调用重载运算符'+'
,因为有一个单独的operatorCallExpr
匹配器来处理重载运算符。
有AST匹配器可以匹配AST的所有不同节点,可以缩小匹配器以仅匹配满足特定条件的AST节点,还可以遍历匹配器以从一种AST节点转移到另一种AST节点。有关AST匹配器的完整列表,请查看AST匹配器参考。
所有作为名词的匹配器都描述AST中的实体,并且可以进行绑定,以便在找到匹配项时都可以引用它们。为此,只需bind
在这些匹配器上调用方法,例如:
variable(hasType(isInteger())).bind("intvar")
步骤2:使用AST匹配器
好的,继续使用匹配器。让我们从定义一个匹配器开始,该匹配器将捕获所有for
定义了初始化为零的新变量的语句。让我们从匹配所有for
循环开始:
forStmt()
接下来,我们要指定在循环的第一部分中声明一个变量,以便将匹配器扩展为
forStmt(hasLoopInit(declStmt(hasSingleDecl(varDecl()))))
最后,我们可以添加将变量初始化为零的条件。
forStmt(hasLoopInit(declStmt(hasSingleDecl(varDecl(
hasInitializer(integerLiteral(equals(0))))))))
阅读和理解匹配器定义非常容易(“匹配循环,其init部分声明了一个初始化为整数常量0的变量”),但是确定每一部分都是必需的。请注意,此匹配不匹配的循环,其变量初始化为'\0'
,0.0
,NULL
,或除了整数0任何形式的零。
最后一步是给匹配器命名并绑定,ForStmt
因为我们要使用它来做一些事情:
StatementMatcher LoopMatcher =
forStmt(hasLoopInit(declStmt(hasSingleDecl(varDecl(
hasInitializer(integerLiteral(equals(0)))))))).bind("forLoop");
定义匹配器后,您将需要添加更多的脚手架才能运行它们。匹配器与配对 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
步骤3.5:更复杂的匹配器
我们的简单匹配器能够发现循环,但是我们仍然需要自己过滤掉更多的循环。我们可以使用一些精挑细选的匹配器来完成剩余工作的很大一部分,但是首先我们需要准确地确定我们要允许的属性。
我们如何表征阵列中可以转换为基于范围的语法的循环?基于范围的循环遍历以下大小的数组N
:
- 从索引开始
0
- 连续迭代
- 结束于索引
N-1
我们已经检查了(1),因此我们需要添加的只是对循环条件的检查,以确保将循环的index变量与之进行比较, N
并进行另一次检查,以确保增量步骤仅对同一变量进行增量。(2)的匹配器很简单:需要在init部分声明的同一变量之前或之后递增。
不幸的是,这样的匹配器是不可能写的。匹配器不包含用于比较两个任意AST节点并确定它们是否相等的逻辑,因此,我们能做的最好的事情就是匹配超出我们希望允许的范围,然后对回调进行额外的比较。
无论如何,我们可以开始构建此子匹配器。我们可以要求增量步长是一元增量,如下所示:
hasIncrement(unaryOperator(hasOperatorName("++")))
指定增量是Clang AST的另一个怪癖:变量的用法表示为DeclRefExpr
的(“声明引用表达式”),因为它们是引用变量声明的表达式。要找到unaryOperator
引用特定声明的,我们可以简单地向其添加第二个条件:
hasIncrement(unaryOperator(
hasOperatorName("++"),
hasUnaryOperand(declRefExpr())))
此外,我们可以限制匹配器仅在增量变量为整数时匹配:
hasIncrement(unaryOperator(
hasOperatorName("++"),
hasUnaryOperand(declRefExpr(to(varDecl(hasType(isInteger())))))))
最后一步是将标识符附加到此变量,以便我们可以在回调中检索它:
hasIncrement(unaryOperator(
hasOperatorName("++"),
hasUnaryOperand(declRefExpr(to(
varDecl(hasType(isInteger())).bind("incrementVariable"))))))
我们可以将此代码添加到的定义中,LoopMatcher
并确保装有新匹配器的程序仅打印出声明单个变量初始化为零并具有由某个变量的一元增量组成的增量步骤的循环。
现在,我们只需要添加一个匹配器来检查for
循环的条件部分是否 将变量与数组的大小进行比较。只有一个问题-在不查看循环主体的情况下,我们不知道要迭代哪个数组!我们再次被限制为使用匹配器逼近所需的结果,在回调中填写详细信息。所以我们开始:
hasCondition(binaryOperator(hasOperatorName("<"))
确保左侧是对变量的引用,并且右侧具有整数类型是有意义的。
hasCondition(binaryOperator(
hasOperatorName("<"),
hasLHS(declRefExpr(to(varDecl(hasType(isInteger()))))),
hasRHS(expr(hasType(isInteger())))))
为什么?因为它不起作用。在提供的三个循环中 test-files/simple.cpp
,有零个具有匹配条件。快速浏览一下循环转换的前一次迭代产生的第一个for循环的AST转储,为我们提供了答案:
(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 ...
我们已经知道声明和增量都匹配,否则就不会转储此循环。罪魁祸首是应用于小于运算符的第一个操作数(即LHS)的隐式强制转换,即将L值转换为R值的表达式应用于引用 i
。幸运的是,匹配器库以的形式提供了针对此问题的解决方案ignoringParenImpCasts
,它指示匹配器在继续匹配之前忽略隐式强制转换和括号。调整条件运算符将恢复所需的匹配。
hasCondition(binaryOperator(
hasOperatorName("<"),
hasLHS(ignoringParenImpCasts(declRefExpr(
to(varDecl(hasType(isInteger())))))),
hasRHS(expr(hasType(isInteger())))))
将绑定添加到我们希望捕获的表达式并将标识符字符串提取到变量后,我们完成了array-step-2。
步骤4:检索匹配的节点
到目前为止,匹配器回调不是很有趣:它只是转储循环的AST。在某些时候,我们将需要对输入源代码进行更改。接下来,我们将继续使用在上一步中绑定的节点。
该MatchFinder::run()
回调需要一个 MatchFinder::MatchResult&
作为参数。我们对其Context
及其Nodes
成员最感兴趣。ASTContext
顾名思义,Clang使用该类来表示有关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将a VarDecl
与每个变量关联以表示变量的声明。由于每个声明的“规范”形式在地址上都是唯一的,因此我们要做的就是确保两个声明ValueDecl
(的基类 VarDecl
)都不相同,NULL
并比较规范的Decls。
static bool areSameVariable(const ValueDecl *First, const ValueDecl *Second) {
return First && Second &&
First->getCanonicalDecl() == Second->getCanonicalDecl();
}
如果执行到达的末尾LoopPrinter::run()
,我们知道循环外壳看起来像
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
然后用作哈希,可以用来比较表达式。我们稍后需要areSameExpr
。在添加到test-files / simple.cpp的其他循环上运行新代码之前,请尝试确定哪些循环可能被转换。