哈喽,各位好!今天咱们来聊聊Clang LibTooling,这玩意儿听起来高大上,实际上就是个基于Clang AST(抽象语法树)的编译期代码分析和生成工具。说白了,就是让你能在编译的时候,像个超级侦探一样,窥探你的代码,还能动手改改它。
为啥要用LibTooling?
你可能会问,我代码都写完了,编译器也能跑,为啥还要搞这么个玩意儿?原因很简单:
- 自动化重构: 想批量改名?想把循环改成并行?手动改?不存在的,LibTooling帮你搞定。
- 静态代码分析: 想找出潜在的Bug?想遵守编码规范?LibTooling帮你检查。
- 代码生成: 想根据现有代码生成新的代码?LibTooling帮你生成。
- 自定义语言扩展: 想给C++加点新特性?LibTooling允许你魔改。
总之,有了LibTooling,你就能在编译阶段对代码进行各种骚操作,解放你的双手,提高你的代码质量。
Clang AST是啥?
要玩转LibTooling,首先要了解Clang AST。你可以把它想象成编译器对你代码的一种内部表示,就像X光片一样,能看到你代码的骨架。
// 源代码
int main() {
int x = 10;
int y = x + 5;
return 0;
}
这段代码会被Clang解析成一个AST,大概长这样(简化版):
- FunctionDecl: main
- CompoundStmt
- DeclStmt
- VarDecl: x
- IntegerLiteral: 10
- VarDecl: x
- DeclStmt
- VarDecl: y
- BinaryOperator: +
- DeclRefExpr: x
- IntegerLiteral: 5
- BinaryOperator: +
- VarDecl: y
- ReturnStmt
- IntegerLiteral: 0
- DeclStmt
- CompoundStmt
这个AST就像一颗树,每个节点代表代码中的一个元素,比如函数声明、变量声明、表达式等等。
LibTooling的基本流程
使用LibTooling进行代码分析和生成,通常需要经过以下几个步骤:
- 创建ToolDriver: 这是LibTooling的入口,负责解析命令行参数,创建编译器实例。
- 创建FrontendAction: 这是你的代码分析逻辑的载体,告诉LibTooling你要做什么。
- 创建ASTConsumer: 这是实际访问AST的组件,你可以在这里遍历AST,分析代码,修改代码。
- 运行ToolDriver: 让ToolDriver驱动整个流程,开始编译和分析代码。
代码示例:查找所有函数调用
咱们先来个简单的例子,用LibTooling查找代码中所有的函数调用。
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/Tooling.h"
#include <iostream>
using namespace clang;
using namespace clang::tooling;
// 1. 创建ASTConsumer
class FindFunctionCallConsumer : public ASTConsumer {
public:
explicit FindFunctionCallConsumer(ASTContext *Context) : Visitor(Context) {}
void HandleTranslationUnit(ASTContext &Context) override {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
}
private:
class FindFunctionCallVisitor : public RecursiveASTVisitor<FindFunctionCallVisitor> {
public:
explicit FindFunctionCallVisitor(ASTContext *Context) : Context(Context) {}
bool VisitCallExpr(CallExpr *Call) {
if (FunctionDecl *Func = Call->getDirectCallee()) {
llvm::outs() << "Function Call: " << Func->getNameInfo().getName().getAsString() << "n";
}
return true;
}
private:
ASTContext *Context;
};
FindFunctionCallVisitor Visitor;
};
// 2. 创建FrontendAction
class FindFunctionCallAction : public ASTFrontendAction {
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler, StringRef InFile) override {
return std::make_unique<FindFunctionCallConsumer>(&Compiler.getASTContext());
}
};
int main(int argc, const char **argv) {
// 3. 创建ToolDriver
CommonOptionsParser OptionsParser(argc, argv, "Find Function Calls");
ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());
// 4. 运行ToolDriver
return Tool.run(newFrontendActionFactory<FindFunctionCallAction>().get());
}
代码解释:
FindFunctionCallConsumer
: 这个类继承自ASTConsumer
,负责处理AST。HandleTranslationUnit
函数会在遍历整个翻译单元(一个源文件)之前被调用,我们在这里启动AST的遍历。FindFunctionCallVisitor
: 这个类继承自RecursiveASTVisitor
,负责递归遍历AST。VisitCallExpr
函数会在每次遇到函数调用表达式时被调用,我们在这里输出函数名。FindFunctionCallAction
: 这个类继承自ASTFrontendAction
,负责创建ASTConsumer
。main
函数:CommonOptionsParser
:解析命令行参数。ClangTool
:创建Clang工具实例,需要编译数据库和源文件路径。Tool.run
:运行Clang工具,传入一个FrontendActionFactory
,用于创建FrontendAction
。
编译和运行:
-
创建
compile_commands.json
: LibTooling需要编译数据库才能正确解析代码。你可以使用CMake
或者Bear
等工具生成compile_commands.json
。# 使用CMake mkdir build cd build cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .. make
或者
# 使用Bear bear -- make
-
编译代码:
clang++ -std=c++17 find_function_calls.cpp -o find_function_calls `llvm-config --cxxflags --ldflags --system-libs --libs core` -I /path/to/llvm/include -I /path/to/clang/include
注意: 你需要根据你的系统配置修改
/path/to/llvm/include
和/path/to/clang/include
。 -
运行代码:
./find_function_calls your_source_file.cpp --
your_source_file.cpp
是你要分析的源文件。
示例代码:
// your_source_file.cpp
#include <iostream>
void foo() {
std::cout << "Hello from foo!n";
}
int main() {
foo();
std::cout << "Hello from main!n";
return 0;
}
预期输出:
Function Call: foo
Function Call: operator<<
代码示例:自动添加代码注释
接下来,咱们来个稍微复杂点的例子,用LibTooling自动给函数添加代码注释。
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/Tooling.h"
#include <iostream>
using namespace clang;
using namespace clang::tooling;
// 1. 创建ASTConsumer
class AddCommentConsumer : public ASTConsumer {
public:
explicit AddCommentConsumer(ASTContext *Context, Rewriter &TheRewriter) : Visitor(Context, TheRewriter), TheRewriter(TheRewriter) {}
void HandleTranslationUnit(ASTContext &Context) override {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
TheRewriter.overwriteChangedFiles();
}
private:
class AddCommentVisitor : public RecursiveASTVisitor<AddCommentVisitor> {
public:
explicit AddCommentVisitor(ASTContext *Context, Rewriter &TheRewriter) : Context(Context), TheRewriter(TheRewriter) {}
bool VisitFunctionDecl(FunctionDecl *Func) {
SourceLocation StartLoc = Func->getSourceRange().getBegin();
// 检查是否已经有注释
if (Func->getBeginLoc().isMacroID() || TheRewriter.getSourceMgr().getPresumedLoc(StartLoc).getColumn() != 1) {
return true; // 跳过宏定义和非行首的函数
}
std::string Comment = "// This function does something.n";
TheRewriter.InsertTextBefore(StartLoc, Comment);
return true;
}
private:
ASTContext *Context;
Rewriter &TheRewriter;
};
AddCommentVisitor Visitor;
Rewriter &TheRewriter;
};
// 2. 创建FrontendAction
class AddCommentAction : public ASTFrontendAction {
public:
AddCommentAction() {}
void setRewriter(Rewriter &TheRewriter) {
this->TheRewriter = &TheRewriter;
}
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler, StringRef InFile) override {
TheRewriter->setSourceMgr(Compiler.getSourceManager(), Compiler.getLangOpts());
return std::make_unique<AddCommentConsumer>(&Compiler.getASTContext(), *TheRewriter);
}
private:
Rewriter *TheRewriter = nullptr;
};
int main(int argc, const char **argv) {
// 3. 创建ToolDriver
CommonOptionsParser OptionsParser(argc, argv, "Add Comments");
ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());
// 创建Rewriter
Rewriter TheRewriter;
// 4. 运行ToolDriver
std::unique_ptr<AddCommentAction> action = std::make_unique<AddCommentAction>();
action->setRewriter(TheRewriter);
int result = Tool.run(std::unique_ptr<FrontendActionFactory>(new FrontendActionFactoryFromProto(std::move(action))).get());
return result;
}
代码解释:
AddCommentConsumer
: 这个类继承自ASTConsumer
,负责处理AST。HandleTranslationUnit
函数会在遍历完整个翻译单元之后被调用,我们在这里调用TheRewriter.overwriteChangedFiles()
,将修改后的代码写回文件。AddCommentVisitor
: 这个类继承自RecursiveASTVisitor
,负责递归遍历AST。VisitFunctionDecl
函数会在每次遇到函数声明时被调用,我们在这里判断函数是否已经有注释,如果没有,则添加注释。AddCommentAction
: 这个类继承自ASTFrontendAction
,负责创建ASTConsumer
。Rewriter
: 这个类是Clang提供的用于修改代码的工具。main
函数:- 创建
Rewriter
实例。 - 将
Rewriter
实例传递给AddCommentAction
。 - 运行Clang工具。
- 创建
编译和运行:
编译步骤与前面的例子类似。
示例代码:
// your_source_file.cpp
#include <iostream>
void foo() {
std::cout << "Hello from foo!n";
}
int main() {
foo();
std::cout << "Hello from main!n";
return 0;
}
修改后的代码:
// your_source_file.cpp
#include <iostream>
// This function does something.
void foo() {
std::cout << "Hello from foo!n";
}
// This function does something.
int main() {
foo();
std::cout << "Hello from main!n";
return 0;
}
需要注意的点:
- 编译数据库: LibTooling依赖编译数据库,所以一定要确保
compile_commands.json
文件是正确的。 - AST遍历: AST的结构比较复杂,需要仔细研究Clang的文档,才能正确地遍历AST。
- 代码修改: 使用
Rewriter
修改代码时,需要注意SourceLocation的正确性,否则可能会导致代码错乱。 - 头文件包含: 根据你的需求,可能需要包含更多的Clang头文件。
更高级的应用
LibTooling的用途远不止这些,你可以用它来做更多的事情:
- 自动生成代码: 根据现有代码生成新的类、函数等。
- 代码格式化: 自动调整代码的缩进、空格等,使其符合编码规范。
- 代码安全检查: 检查代码中是否存在潜在的安全漏洞。
- 自定义语言扩展: 给C++添加新的语法特性。
表格总结
组件 | 功能 |
---|---|
ToolDriver |
LibTooling的入口,负责解析命令行参数,创建编译器实例。 |
FrontendAction |
代码分析逻辑的载体,告诉LibTooling你要做什么。 |
ASTConsumer |
实际访问AST的组件,你可以在这里遍历AST,分析代码,修改代码。 |
RecursiveASTVisitor |
递归遍历AST的工具类,提供了方便的接口用于访问AST节点。 |
Rewriter |
修改源代码的工具类,可以插入、删除、替换代码。 |
ASTContext |
存储AST信息的上下文,包含了类型信息、声明信息等。 |
SourceManager |
管理源代码的工具类,可以获取源代码的文本、位置等信息。 |
LangOptions |
语言选项,例如C++标准、是否启用扩展等。 |
学习资源
- Clang官方文档: https://clang.llvm.org/
- LLVM官方文档: https://llvm.org/
- Clang LibTooling Examples: https://github.com/llvm/llvm-project/tree/main/clang-tools-extra/examples/clang-tidy/checkers
总结
Clang LibTooling是一个强大的工具,可以让你在编译阶段对代码进行各种骚操作。虽然学习曲线比较陡峭,但是一旦掌握,就能极大地提高你的开发效率和代码质量。希望今天的分享能帮助你入门LibTooling,开启你的代码魔改之旅!
最后,别忘了多看文档,多写代码,实践才是检验真理的唯一标准! 加油!