好的,各位观众老爷们,大家好!今天咱们来聊聊一个听起来就很高大上,但其实也没那么可怕的技术——C++ Clang Reflection API,也就是利用Clang AST进行编译期反射。
什么是编译期反射?
首先,咱们得搞清楚啥是反射。简单来说,反射就是程序在运行时检查自身结构的能力,比如知道自己有哪些类、哪些成员变量、哪些方法等等。这在动态语言里很常见,像Java、Python都支持。
但是,C++这老家伙,天生就是个静态类型语言,它的哲学是尽可能把检查都放在编译期,运行时就别瞎折腾了。所以,传统的C++是不支持反射的。
那编译期反射又是啥呢?就是把反射的功能搬到编译期去做!这样,我们就可以在编译的时候,就拿到C++代码的结构信息,然后根据这些信息生成一些代码,做一些骚操作。
为什么要用Clang AST?
C++编译的过程大致是:预处理 -> 编译 -> 汇编 -> 链接。其中,“编译”这个步骤,编译器会把C++代码转换成一种中间表示,叫做抽象语法树(Abstract Syntax Tree,简称AST)。AST就像一颗树,把代码的结构给完整地表示出来。
Clang就是个C++编译器,而且它很良心的提供了API,让我们能够访问和操作这个AST!这就意味着,我们可以在编译的时候,通过Clang API拿到C++代码的AST,然后分析这棵树,提取出我们需要的信息,比如类名、成员变量、方法等等。
Clang Reflection API能干啥?
有了编译期反射,我们就能做很多有趣的事情:
- 自动序列化/反序列化: 根据类的结构,自动生成序列化和反序列化的代码,省去手动编写的麻烦。
- 自动生成代码: 根据类的定义,自动生成访问器(getter/setter)、构造函数、拷贝构造函数等等,减少重复代码。
- 实现依赖注入: 在编译期分析类的依赖关系,自动生成依赖注入的代码。
- 实现ORM (Object-Relational Mapping): 根据类的定义,自动生成数据库表的映射代码。
- 代码生成工具: 总而言之,只要你有需要根据代码结构生成代码的场景,Clang Reflection API就能帮上忙。
准备工作:搭建环境
要玩转Clang Reflection API,你需要先搭建好环境。
- 安装Clang: 这个不用多说,你得先有个Clang编译器。具体安装方法根据你的操作系统来定,网上有很多教程,搜一下就知道了。
- 安装LLVM/Clang开发库: 你需要安装LLVM和Clang的开发库,这样才能在你的代码里使用Clang API。这个安装方法也根据操作系统来定,同样可以搜到教程。
- 一个C++项目: 最好用CMake管理你的项目,方便配置编译选项。
代码示例:Hello, Reflection!
咱们先来个简单的例子,看看怎么用Clang API获取类的名字。
#include <iostream>
#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>
using namespace clang;
using namespace clang::tooling;
// 自定义AST访问者,用于查找类定义
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:
explicit MyASTVisitor(ASTContext *Context) : Context(Context) {}
// 找到类定义时调用
bool VisitCXXRecordDecl(CXXRecordDecl *Declaration) {
if (Declaration->isCompleteDefinition()) {
// 获取类的名字
auto ClassName = Declaration->getNameAsString();
std::cout << "Found class: " << ClassName << std::endl;
}
return true;
}
private:
ASTContext *Context;
};
// 自定义AST消费者,用于创建AST访问者
class MyASTConsumer : public ASTConsumer {
public:
explicit MyASTConsumer(ASTContext *Context) : Visitor(Context) {}
// 处理编译单元
void HandleTranslationUnit(ASTContext &Context) override {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
}
private:
MyASTVisitor Visitor;
};
// 自定义前端动作,用于创建AST消费者
class MyFrontendAction : public ASTFrontendAction {
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
StringRef file) override {
return std::make_unique<MyASTConsumer>(&CI.getASTContext());
}
};
int main(int argc, const char **argv) {
if (argc < 2) {
std::cerr << "Usage: reflection <source_file>" << std::endl;
return 1;
}
// 创建CommandLineArguments
std::vector<std::string> args(argv + 1, argv + argc);
// 创建ClangTool
ClangTool Tool(CommonOptionsParser::create(args).getCompilations(), args);
// 运行ClangTool
return Tool.run(newFrontendActionFactory<MyFrontendAction>().get());
}
代码解释:
- 头文件: 引入Clang API相关的头文件。
MyASTVisitor
: 自定义AST访问者,继承自RecursiveASTVisitor
。它会递归地遍历AST,找到所有的类定义(CXXRecordDecl
),然后获取类的名字。MyASTConsumer
: 自定义AST消费者,用于创建MyASTVisitor
。它会在处理编译单元时,调用MyASTVisitor
来遍历AST。MyFrontendAction
: 自定义前端动作,用于创建MyASTConsumer
。它是Clang Tool的入口点。main
函数:- 创建
ClangTool
,这是Clang Tool的入口点。 - 调用
Tool.run
,启动Clang Tool,它会执行我们自定义的前端动作,从而遍历AST,找到所有的类定义。
- 创建
编译和运行:
- 把上面的代码保存为
reflection.cpp
。 - 创建一个C++源文件,比如
test.cpp
,里面定义一些类:
// test.cpp
class MyClass {
public:
int x;
void foo() {}
};
class AnotherClass {
public:
double y;
};
- 使用CMake编译
reflection.cpp
:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(reflection)
find_package(LLVM REQUIRED CONFIG)
find_package(Clang REQUIRED CONFIG)
include_directories(${LLVM_INCLUDE_DIRS} ${Clang_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
add_executable(reflection reflection.cpp)
target_link_libraries(reflection clangTooling clangAST clangFrontend clangDriver clangBasic)
- 编译和运行:
mkdir build
cd build
cmake ..
make
./reflection test.cpp
如果一切顺利,你会在控制台看到:
Found class: MyClass
Found class: AnotherClass
更进一步:获取成员变量信息
光知道类的名字还不够,咱们还得知道类的成员变量的信息。修改MyASTVisitor
,添加获取成员变量的代码:
// 自定义AST访问者,用于查找类定义
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:
explicit MyASTVisitor(ASTContext *Context) : Context(Context) {}
// 找到类定义时调用
bool VisitCXXRecordDecl(CXXRecordDecl *Declaration) {
if (Declaration->isCompleteDefinition()) {
// 获取类的名字
auto ClassName = Declaration->getNameAsString();
std::cout << "Found class: " << ClassName << std::endl;
// 遍历类的成员
for (auto Field : Declaration->fields()) {
// 获取成员变量的名字
auto FieldName = Field->getNameAsString();
// 获取成员变量的类型
auto FieldType = Field->getType().getAsString();
std::cout << " Member: " << FieldName << " : " << FieldType << std::endl;
}
}
return true;
}
private:
ASTContext *Context;
};
代码解释:
- 在
VisitCXXRecordDecl
函数中,我们遍历类的所有成员变量(Declaration->fields()
)。 - 对于每个成员变量,我们获取它的名字(
Field->getNameAsString()
)和类型(Field->getType().getAsString()
)。
重新编译和运行,你会看到:
Found class: MyClass
Member: x : int
Found class: AnotherClass
Member: y : double
更更进一步:自动生成Getter/Setter
现在,咱们来个更有趣的,根据类的成员变量,自动生成getter和setter方法。修改MyASTVisitor
,添加生成getter/setter的代码:
// 自定义AST访问者,用于查找类定义
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:
explicit MyASTVisitor(ASTContext *Context) : Context(Context), SourceManager(Context->getSourceManager()) {}
// 找到类定义时调用
bool VisitCXXRecordDecl(CXXRecordDecl *Declaration) {
if (Declaration->isCompleteDefinition() && Declaration->isClass()) {
// 获取类的名字
auto ClassName = Declaration->getNameAsString();
std::cout << "Found class: " << ClassName << std::endl;
// 遍历类的成员
for (auto Field : Declaration->fields()) {
// 获取成员变量的名字
auto FieldName = Field->getNameAsString();
// 获取成员变量的类型
auto FieldType = Field->getType().getAsString();
std::cout << " Member: " << FieldName << " : " << FieldType << std::endl;
// 生成getter方法
std::string GetterName = "get" + capitalize(FieldName);
std::string GetterCode = generateGetter(ClassName, FieldName, FieldType, GetterName);
std::cout << " Generated getter: " << GetterName << std::endl;
insertCode(Declaration->getSourceRange().getEnd(), GetterCode);
// 生成setter方法
std::string SetterName = "set" + capitalize(FieldName);
std::string SetterCode = generateSetter(ClassName, FieldName, FieldType, SetterName);
std::cout << " Generated setter: " << SetterName << std::endl;
insertCode(Declaration->getSourceRange().getEnd(), SetterCode);
}
}
return true;
}
private:
ASTContext *Context;
SourceManager &SourceManager;
// 首字母大写
std::string capitalize(const std::string& str) {
if (str.empty()) {
return str;
}
std::string result = str;
result[0] = toupper(result[0]);
return result;
}
// 生成getter方法
std::string generateGetter(const std::string& className, const std::string& fieldName, const std::string& fieldType, const std::string& getterName) {
return "n " + fieldType + " " + getterName + "() const { return " + fieldName + "; }n";
}
// 生成setter方法
std::string generateSetter(const std::string& className, const std::string& fieldName, const std::string& fieldType, const std::string& setterName) {
return "n void " + setterName + "(" + fieldType + " value) { " + fieldName + " = value; }n";
}
// 插入代码
void insertCode(SourceLocation location, const std::string& code) {
SourceManager.getEditBuffer(location.getFileID()).insertString(location, code);
}
};
代码解释:
- 添加了
capitalize
函数,用于将字符串的首字母大写。 - 添加了
generateGetter
和generateSetter
函数,用于生成getter和setter方法的代码。 - 在
VisitCXXRecordDecl
函数中,我们调用generateGetter
和generateSetter
生成getter和setter方法的代码,并打印到控制台。- 使用
SourceManager.getEditBuffer
与insertString
函数将生成的代码插入源文件。
- 使用
注意:
- 这个例子只是一个简单的演示,生成的getter/setter代码很简单。
- 这个例子直接修改了源文件,实际应用中,你可能需要更复杂的方式来处理生成的代码,比如生成一个单独的文件。
- 要使代码能够正确插入,需要修改
MyFrontendAction
的CreateASTConsumer
函数,添加以下代码:
class MyFrontendAction : public ASTFrontendAction {
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
StringRef file) override {
CI.getFrontendOptions().AutoAdjustSourceOffsets = false;
CI.getFrontendOptions().UsePrebuiltPCH = false;
return std::make_unique<MyASTConsumer>(&CI.getASTContext());
}
void EndSourceFileAction() override {
SourceManager &SM = getCompilerInstance().getSourceManager();
FileID mainFileID = SM.getMainFileID();
// 输出修改后的代码
SM.getEditBuffer(mainFileID).write(llvm::outs());
}
};
重新编译和运行,你会看到控制台输出getter/setter方法的代码,并且test.cpp
文件会被修改,自动添加了getter和setter方法:
// test.cpp
class MyClass {
public:
int x;
void foo() {}
int getX() const { return x; }
void setX(int value) { x = value; }
};
class AnotherClass {
public:
double y;
double getY() const { return y; }
void setY(double value) { y = value; }
};
总结:
今天,我们一起简单地了解了C++ Clang Reflection API,并通过几个例子演示了如何使用Clang API获取类的名字、成员变量的信息,以及如何自动生成getter/setter方法。
Clang Reflection API是一个非常强大的工具,它可以帮助我们实现很多有趣的功能,提高开发效率。当然,它的学习曲线也比较陡峭,需要你花一些时间去学习和实践。
希望今天的分享对你有所帮助,感谢大家!