01 - 获取反射信息
尝试从零实现(抄)一个反射系统,并应用于我酝酿中的游戏引擎项目里!本节将实现反射系统的第一部分:获取反射信息。
本节内容组织的大体思路如下:
- 配置LLVM环境(主要是Clang);
- 用clang获取抽象语法树AST信息,了解语法树常用节点;
- 简要介绍Index,TranslationUnit,Type,Cursor,尝试在C++中调用clang,并完成上述步骤;
环境配置
首先要配置LLVM-Clang环境,因为下文将要用它来分析代码的抽象语法树AST信息。环境配置的方法有如下两种:
- 编译LLVM-Clang:这里由于我电脑的配置不够,在编译的时候老出现内存溢出,就放弃了。如果想自己编译LLVM-Clang的话可以参考它的官方文档,知乎上也有不少教程。
- 下载安装包:如果没有自定义或编译LLVM的需求,可以去官网上直接下载它的二进制安装包。
然后需要在相关目录中获得如下文件,然后链接到自己项目中:
获取AST
接下来需要用Clang获取代码中的AST信息,首先编写代码:
class Student
{
private:
int mClassID;
bool mGender;
public:
void SetClassID(int Val) { mClassID = Val; }
void SetGender(bool val) { mGender = val; }
int GetClassID() const { return mClassID; }
bool GetGender() const { return mGender; }
};
然后用Clang获取它的AST信息:
clang -Xclang -ast-dump -fsyntax-only -x c++ -std=c++20 student.h
部分输出如下:
可以发现我们成功获取了Student类的信息,包括类名、成员变量和方法。最左边的内容是AST树的节点,一些AST树节点的意义如下:
节点类型 | 意义 |
---|---|
TranslationUnitDecl | Clang AST的顶层节点,遍历AST实际上就是对TranslationUnitDecl的子节点进行遍历 |
CompoundStmt | 代码块,函数实现、struct、enum、for的body一般会用它包起来 |
DeclStmt | 定义语句,VarDecl等类型的定义一般会用它包起来 |
VarDecl | 变量定义语句 |
MethodDecl | 函数定义语句 |
FieldDecl | 成员变量定义语句 |
IfStmt | if语句,包括Cond、TrueBody、FalseBody三部分。 |
ForStmt | for语句 |
UnaryOperator | 一元操作符 |
BinaryOperator | 二元操作符,包括=、>、<、<=、>=、==等各种二元操作 |
ImplicitCastExpr | 隐式转换表达式 |
CallExpr | 函数调用表达式 |
ReturnStmt | 函数返回语句 |
ParenExpr | 括号表达式 |
TypedefDecl | 类型转换语句,如遇指针类型会内建PointerType和BuiltinType实现转换 |
RecordDecl | class或struct的定义,使用InitExpr或InitListExpr初始化成员 |
AccessSpecDecl | 类的public、private、protected访问权限 |
Constructor、Destructor | 类的构造与析构函数 |
Literal | 不同类型的字面量,包含IntegerLiteral和FloatingLiteral等 |
在C++中调用Clang
Clang数据结构
在这样做之前先了解一下待会可能用到的Clang数据结构:
CXCursor
是指向语法树节点的游标,可以用它来遍历语法树,访问节点信息。它的常用API如下:
// 获取当前cursor的种类CXCursorKind
// 也就是获取当前AST节点的类型
CXCursorKind kind = clang_getCursorKind(cursor);
// 获取当前cursor节点的名字 (全名)
// 例如变量名, 完整函数签名
CXString cxstr = clang_getCursorSpelling(cursor);
// 获取当前cursor节点的名字 (简短名)
// 例如函数名字(没参数)
CXString cxstr = clang_getCursorDisplayName(cursor);
// 获取当前cursor节点用于链接的名字
CXString cxstr = clang_Cursor_getMangling(cursor);
// 获取当前cursor节点元素的类型CXType
// 变量声明节点是变量的类型
// 字段声明节点是字段的类型
CXType type = clang_getCursorType(cursor);
// 获取当前cursor节点的位置信息CXSourceLocation
// 在源码中的行数, 列数, 文件名等
CXSourceLocation location = clang_getCursorLocation(cursor);
// 获取当前cursor节点的起止位置范围信息
CXSourceRange range = clang_getCursorExtent(cursor);
// 获取当前cursor节点的访问权限
// public, protected, private, none, invalid
CX_CXXAccessSpecifier access = clang_getCXXAccessSpecifier(cursor);
// 对cursor子节点的遍历逻辑
// cursor: 正在遍历的cursor
// parent: cursor的父节点
// data: 外部传参
CXCursorVistor childVistor = [](CXCursor cursor, CXCursor parent, CXClentData data)
{
// 在该节点的操作
// 决定遍历策略(Break, Continue, Recurse)
return CXChildVIsit_xxxxxxx;
};
// 开始对rootCursor的子节点遍历
// 如果要外部传参, 就在nullptr里填参数
clang_visitChildren(rootCursor, childVisitor, nullptr);
CXType
如果该节点有类型,就代表该节点的类型。它的常用属性如下:
CXType test;
test.kind; // CXTypeKind枚举, 类型的类型, 如INT, FLOAT等
它的常用API如下:
// 获取类型的名字, 如int, float
CXString cxstr = clang_getTypeSpelling(type);
// 获取类型的对齐, 大小, 字段偏移量
long long val = clang_Type_getAlignOf(type);
long long val2 = clang_Type_getSizeOf(type);
long long val3 = clang_Type_getOffsetOf(type, fieldName);
CXTranslationUnit
编译单元,即一个C++源文件。它的常用API如下:
// 获取该编译单元的根节点, 类型为TRANSLATION_UNIT的Cursor
CXCursor cursor = clang_getTranslationUnitCursor(translator);
// 获取该编译单元的文件名
clang_getTranslationUnitSpelling(translator);
CXIndex
一个CXIndex就是一个CXTranslationUnit的集合,并且最终被链接到一起,形成一个可执行文件或者库。
可以使用如下方法创建一个CXIndex:
// 创建一个新的CXIndex
CXIndex index = clang_createIndex(0, 0);
// 用Parse解析源文件后返回的CXTranslationUnit
CXTranslationUnit translator = nullptr;
std::vector<const char*> arguments = {"-x",
"c++",
"-std=c++20",
"-DNDEBUG",
"-D__clang__",
"-w",
"-MG",
"-M",
"-ferror-limit=0",
"-o clangLog.txt"};
std::string filePath = std::string(TESTFILE_PATH).append("student.h");
// 解析一个C++源文件, 返回错误代码
// 参数: CIdx index自身
// source_filename 源文件路径
// command_line_args 命令行参数, 就是上文使用clang时的命令行参数
// num_command_line_args 命令行参数个数
// unsaved_files 未保存的文件
// num_unsaved_files 未保存文件个数
// options 定制解析过程的参数, 例如SkipFunctionBodies
// out_TU 传入传出的CXTranslationUnit
CXErrorCode error = clang_parseTranslationUnit2(index,
filePath.c_str(),
arguments.data(),
(int)arguments.size(),
nullptr,
0,
CXTranslationUnit_None,
&translator);
使用编译器标记
但我们并不想要获取所有的AST树节点信息,因此可以利用Clang的编译器标记来告诉我们哪些节点该获取:
// 经典写法
__attribute__((annotate("xxx")))
// C++11写法
[[clang::annotate("xxx")]]
修改Student类的代码如下:
// Student.h
#ifdef __REFLECTION_PARSER__
#define REFLECTION_API __attribute__((annotate("reflect-class")))
#define REFLECTION_PROPERTY(...) __attribute__((annotate("reflect-property; " #__VA_ARGS__)))
#define REFLECTION_FUNCTION(...) __attribute__((annotate("reflect-function; " #__VA_ARGS__)))
#else
#define REFLECTION_API
#define REFLECTION_PROPERTY(...)
#define REFLECTION_FUNCTION(...)
#endif // __REFLECTION_PARSER__
class REFLECTION_API Student
{
private:
REFLECTION_PROPERTY(BlueprintEditOnly; Category = 123)
int mClassID;
REFLECTION_PROPERTY()
bool mGender;
public:
REFLECTION_FUNCTION()
void SetClassID(int Val) { mClassID = Val; }
REFLECTION_FUNCTION()
void SetGender(bool val) { mGender = val; }
REFLECTION_FUNCTION()
int GetClassID() const { return mClassID; }
bool GetGender() const { return mGender; }
};
使用Clang获取的AST节点信息如下(省略非必要信息):
`-CXXRecordDecl 0x170c5254a40 <.\TestFile\student.h:13:1, line:33:1> line:13:22 class Student definition
|-AnnotateAttr 0x170c5254b78 <line:4:39, col:63> "reflect-class"
|-FieldDecl 0x170c5254d68 <line:5:34, line:17:9> col:9 referenced mClassID 'int'
| `-AnnotateAttr 0x170c5254dc0 <line:5:49, col:91> "reflect-property; BlueprintEditOnly; Category = 123"
|-FieldDecl 0x170c5254ec8 <col:34, line:20:10> col:10 referenced mGender 'bool'
| `-AnnotateAttr 0x170c5254f20 <line:5:49, col:91> "reflect-property; "
|-CXXMethodDecl 0x170c5255178 <line:6:34, line:24:48> col:10 SetClassID 'void (int)' implicit-inline
| |-ParmVarDecl 0x170c5255048 <col:21, col:25> col:25 used Val 'int'
| `-AnnotateAttr 0x170c5255228 <line:6:49, col:91> "reflect-function; "
|-CXXMethodDecl 0x170c5255428 <col:34, line:27:47> col:10 SetGender 'void (bool)' implicit-inline
| `-AnnotateAttr 0x170c52554d8 <line:6:49, col:91> "reflect-function; "
|-CXXMethodDecl 0x170c5255660 <col:34, line:30:47> col:9 GetClassID 'int () const' implicit-inline
| `-AnnotateAttr 0x170c5255708 <line:6:49, col:91> "reflect-function; "
可以发现编译器标记已经打上了,并且没打上标记的GetGender()
函数被省略了,可以通过获取编译器标记Cursor的父Cursor来得到要反射的信息。
遍历AST
接下来就能编写代码遍历AST了:
#include <filesystem>
#include <format>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <clang-c/Index.h>
// 输出CXString内的信息
std::string GetClangString(const CXString& cxstr)
{
std::string str = clang_getCString(cxstr);
clang_disposeString(cxstr);
return str;
}
std::ostream& operator<<(std::ostream& stream, const CXString& cxstr)
{
std::string str = GetClangString(cxstr);
stream << str;
return stream;
}
int main()
{
std::vector<const char*> arguments = {"-x",
"c++",
"-std=c++20",
"-D__REFLECTION_PARSER__",
"-DNDEBUG",
"-D__clang__",
"-w",
"-MG",
"-M",
"-ferror-limit=0",
"-o clangLog.txt"};
std::string filePath = std::string(TESTFILE_PATH).append("student.h");
CXIndex index = clang_createIndex(0, 0);
CXTranslationUnit translator = nullptr;
CXErrorCode error = clang_parseTranslationUnit2(index,
filePath.c_str(),
arguments.data(),
(int)arguments.size(),
nullptr,
0,
CXTranslationUnit_None,
&translator);
if (error != CXError_Success)
{
std::cout << "Failed to parse translation unit. Error code: " << std::endl;
}
else
{
std::cout << "Translation unit parsed successfully!" << std::endl;
CXCursor rootCursor = clang_getTranslationUnitCursor(translator);
auto childVisitor = [](CXCursor cursor, CXCursor parent, CXClientData data) {
auto cursors = reinterpret_cast<std::vector<CXCursor>*>(data);
if (clang_getCursorKind(cursor) == CXCursor_AnnotateAttr)
{
if (GetClangString(clang_getCursorSpelling(cursor)) == "reflect-class")
{
cursors->push_back(parent);
}
}
return CXChildVisit_Recurse;
};
std::vector<CXCursor> metaCursors;
clang_visitChildren(rootCursor, childVisitor, reinterpret_cast<CXClientData>(&metaCursors));
struct MetaData
{
std::string key;
std::string value;
std::string marcoData;
std::string extraData;
};
std::unordered_map<std::string, std::vector<MetaData>> metaData;
for (auto& cursor : metaCursors)
{
auto visitor = [](CXCursor cursor, CXCursor parent, CXClientData data) {
auto rawData = reinterpret_cast<std::vector<MetaData>*>(data);
if (clang_getCursorKind(cursor) == CXCursor_AnnotateAttr)
{
MetaData meta;
meta.marcoData = clang_getCString(clang_getCursorSpelling(cursor));
if (clang_getCursorKind(parent) == CXCursor_FieldDecl) // reflect-property
{
meta.extraData = meta.marcoData.substr(17);
meta.marcoData = meta.marcoData.substr(0, 17);
meta.key = "field [" + GetClangString(clang_getTypeSpelling(clang_getCursorType(parent))) + "]";
}
else if (clang_getCursorKind(parent) == CXCursor_CXXMethod) // reflect-function
{
meta.extraData = meta.marcoData.substr(17);
meta.marcoData = meta.marcoData.substr(0, 17);
meta.key =
"method [" + GetClangString(clang_getTypeSpelling(clang_getCursorType(parent))) + "]";
}
else
return CXChildVisit_Recurse;
meta.value = GetClangString(clang_getCursorSpelling(parent));
rawData->push_back(meta);
}
return CXChildVisit_Recurse;
};
std::vector<MetaData> data;
clang_visitChildren(cursor, visitor, reinterpret_cast<CXClientData>(&data));
metaData[GetClangString(clang_getCursorSpelling(cursor))] = data;
}
for (const auto& [name, metaDataList] : metaData)
{
std::cout << "Class name: " << name << "\n";
for (const auto& meta : metaDataList)
{
std::cout << std::format("Type: {}\nName: {}\nMacroData: {}\nExtraData: {}",
meta.key,
meta.value,
meta.marcoData,
meta.extraData)
<< "\n";
}
}
clang_disposeTranslationUnit(translator);
clang_disposeIndex(index);
}
return 0;
}
输出如下:
Translation unit parsed successfully!
Class name: Student
Type: field [int]
Name: mClassID
MacroData: reflect-property;
ExtraData: BlueprintEditOnly; Category = 123
Type: field [bool]
Name: mGender
MacroData: reflect-property;
ExtraData:
Type: method [void (int)]
Name: SetClassID
MacroData: reflect-function;
ExtraData:
Type: method [void (bool)]
Name: SetGender
MacroData: reflect-function;
ExtraData:
Type: method [int () const]
Name: GetClassID
MacroData: reflect-function;
ExtraData:
参考资料
- 使用 Clang 工具自由的支配 C++ 代码吧 · ykiko’s blog
- 使用模板技巧和LibClang实现简易C++静态反射系统 - 知乎 (zhihu.com)
- BoomingTech/Piccolo: Piccolo (formerly Pilot) – mini game engine for games104 (github.com)