本文档旨在介绍如何基于 Clang 的 LibTooling 构建一个有用的源代码到源代码翻译工具。它明确面向 Clang 的新用户,因此您只需具备 C++ 和命令行的工作知识即可。
为了使用该编译器,您需要掌握一些抽象语法树(AST)的基本知识。为此,建议读者先浏览一下 Clang AST 简介
步骤 0:获取 Clang
由于 Clang 是 LLVM 项目的一部分,因此您需要先下载 LLVM 的源代码。Clang 和 LLVM 位于同一个 git 仓库,但在不同的目录下。更多信息,请参阅入门指南
mkdir ~/clang-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
./configure.py --bootstrap
sudo cp ninja /usr/bin/
cd ~/clang-llvm
git clone https://gitlab.kitware.com/cmake/cmake.git
cd cmake
git checkout next
./bootstrap
make
sudo make install
好的。现在我们来构建 Clang!
cd ~/clang-llvm
mkdir build && cd build
cmake -G Ninja ../llvm-project/llvm -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra" -DCMAKE_BUILD_TYPE=Release -DLLVM_BUILD_TESTS=ON
ninja
ninja check # Test LLVM only.
ninja clang-test # Test Clang only.
ninja install
我们正在运行
所有测试都会通过。
最后,我们要将 Clang 设置为自己的编译器。
cd ~/clang-llvm/build
ccmake ../llvm-project/llvm
第二条命令将弹出配置 Clang 的图形用户界面。你需要设置 CMAKE_CXX_COMPILER 的条目。按 "t "打开高级模式。向下滚动到 CMAKE_CXX_COMPILER,将其设置为 /usr/bin/clang++,或者你安装它的位置。按 "c "键配置,然后按 "g "键生成 CMake 文件。
最后运行一次 ninja,就大功告成了。
步骤 1:创建 ClangTool
现在我们有了足够的背景知识,是时候创建一个最简单、最有效的 ClangTool 了:语法检查程序。虽然它已经以 clang-check 的形式存在,但我们还是有必要了解其中的奥秘。
首先,我们需要为工具创建一个新目录,并告知 CMake 它的存在。由于这不是一个核心的 clang 工具,它将存在于 clang-tools-extra 仓库中。
cd ~/clang-llvm/llvm-project
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
clangAST
clangASTMatchers
clangBasic
clangFrontend
clangSerialization
clangTooling
)
完成这些后,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) {
auto ExpectedParser = CommonOptionsParser::create(argc, argv, MyToolCategory);
if (!ExpectedParser) {
// Fail gracefully for unsupported options.
llvm::errs() << ExpectedParser.takeError();
return 1;
}
CommonOptionsParser& OptionsParser = ExpectedParser.get();
ClangTool Tool(OptionsParser.getCompilations(),
OptionsParser.getSourcePathList());
return Tool.run(newFrontendActionFactory<clang::SyntaxOnlyAction>().get());
就是这样!你可以从构建目录中运行ninja,编译我们的新工具。
cd ~/clang-llvm/build
ninja
语法检查程序位于 ~/clang-llvm/build/bin 中,现在应该可以在任何源文件上运行了。试试看
echo "int main() { return 0; }" > test.cpp
bin/loop-convert test.cpp --
注意指定源文件后的两个破折号。编译器的附加选项是在破折号后传递的,而不是从编译数据库中加载,因为现在不需要任何选项。
插曲:学习 AST 匹配器基础知识
Clang 最近引入了 ASTMatcher 库,为描述 AST 中的特定模式提供了一种简单、强大而简洁的方法。匹配器通过宏和模板(如果您想知道,请参阅 ASTMatchers.h)作为 DSL 实现,提供了函数式编程语言中常见的代数数据类型的感觉。
例如,假设你只想检查二进制运算符。有一个匹配器可以做到这一点,它被方便地命名为 binaryOperator。我让你猜猜这个匹配器是做什么的:
binaryOperator(hasOperatorName("+"), hasLHS(integerLiteral(equals(0))))
令人震惊的是,它将匹配左侧为 0 的加法表达式。它不会匹配 0 的其他形式,如"\0 "或 NULL,但会匹配扩展为 0 的宏。该匹配器也不会匹配对重载运算符 "+"的调用,因为有一个单独的 operatorCallExpr 匹配器来处理重载运算符。
AST 匹配器可以匹配 AST 的所有不同节点,缩小匹配器可以只匹配符合特定条件的 AST 节点,遍历匹配器可以从一种 AST 节点遍历到另一种 AST 节点。有关 AST 匹配器的完整列表,请查看 AST 匹配器参考资料
所有匹配器都是名词,用于描述 AST 中的实体,并且可以绑定,因此只要找到匹配,就可以引用它们。为此,只需在这些匹配器上调用绑定方法,例如
variable(hasType(isInteger())).bind("intvar")
步骤 2:使用 AST 匹配器
好了,开始真正使用匹配器了。我们先定义一个匹配器,它将捕获所有定义了初始化为 0 的新变量的 for 语句。让我们从匹配所有 for 循环开始:
forStmt()
接下来,我们要指定在循环的第一部分声明一个变量,因此我们可以将匹配器扩展为
forStmt(hasLoopInit(declStmt(hasSingleDecl(varDecl()))))
最后,我们可以添加变量初始化为零的条件。
forStmt(hasLoopInit(declStmt(hasSingleDecl(varDecl(
hasInitializer(integerLiteral(equals(0))))))))
阅读并理解匹配器的定义("匹配 init 部分声明了一个初始化为整数字面量 0 的单一变量的循环")相当容易,但确定每一个部分都是必要的就比较困难了。请注意,这个匹配器不会匹配那些变量初始化为"\0"、0.0、NULL 或除整数 0 之外任何形式的 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) {
auto ExpectedParser = CommonOptionsParser::create(argc, argv, MyToolCategory);
if (!ExpectedParser) {
// Fail gracefully for unsupported options.
llvm::errs() << ExpectedParser.takeError();
return 1;
}
CommonOptionsParser& OptionsParser = ExpectedParser.get();
ClangTool Tool(OptionsParser.getCompilations(),
OptionsParser.getSourcePathList());
LoopPrinter Printer;
MatchFinder Finder;
Finder.addMatcher(LoopMatcher, &Printer);
return Tool.run(newFrontendActionFactory(&Finder).get());
}
现在,你应该可以重新编译并运行代码来发现 for 循环了。创建一个包含几个示例的新文件,并测试我们的新作品:
cd ~/clang-llvm/build/
ninja loop-convert
vim ~/test-files/simple-loops.cc
bin/loop-convert ~/test-files/simple-loops.cc
步骤 3.5:更复杂的匹配器
我们的简单匹配器能够发现 for 循环,但我们仍然需要自己过滤掉更多的循环。我们可以通过一些巧妙选择的匹配器来完成剩余的大部分工作,但首先我们需要确定我们要允许哪些属性。
我们该如何描述数组上的 for 循环的特性,以便将其转换为基于范围的语法?大小为 N 的数组上的基于范围的循环,即
从索引 0 开始
连续遍历
在索引 N-1 处结束
我们已经检查了 (1),因此只需在循环的条件中添加一项检查,以确保将循环的索引变量与 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中提供的三个循环中,没有一个的条件是匹配的。通过之前版本的loop-convert产生的第一个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 ...
我们已经知道声明和增量都匹配,否则这个循环不会被转储。问题出在应用于小于操作符第一个操作数(即左手边)的隐式转换上,一个将引用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 成员。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() 的末尾,我们知道循环的外壳看起来像
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 中一个节点的描述。然后,FoldingSetNodeID 作为我们用来比较表达式的哈希值。我们稍后会需要 areSameExpr。在你在 test-files/simple.cpp 添加的额外循环上运行新代码之前,尝试弄清楚哪些将被认为是潜在可转换的。