使用Clang作为库 —— 使用 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.0NULL或除整数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很简单:需要在初始化部分中声明的相同变量的前置或后置增量。
不幸的是,不可能编写这样的matchermatcher不包含用于比较任意两个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-valueR-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&作为参数。我们最感兴趣的是它的ContextNodes成员。正如名称所示,Clang使用ASTContext类来表示AST的上下文信息,尽管功能上最重要的细节是一些操作需要一个ASTContext*参数。更直接有用的是一组匹配的节点,以及我们如何检索它们。

由于绑定了三个变量(由ConditionVarNameInitVarNameIncrementVarName标识),所以可以使用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与每个变量关联起来,以表示变量的声明。由于每个声明的“规范”形式按地址是唯一的,所以我们需要做的就是确保ValueDeclVarDecl的基类)都不是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的额外循环运行新代码之前,试着找出哪些可能被认为是可转换的。

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值