01 - 获取反射信息

尝试从零实现(抄)一个反射系统,并应用于我酝酿中的游戏引擎项目里!本节将实现反射系统的第一部分:获取反射信息。

本节内容组织的大体思路如下:

  1. 配置LLVM环境(主要是Clang);
  2. 用clang获取抽象语法树AST信息,了解语法树常用节点;
  3. 简要介绍Index,TranslationUnit,Type,Cursor,尝试在C++中调用clang,并完成上述步骤;

环境配置

首先要配置LLVM-Clang环境,因为下文将要用它来分析代码的抽象语法树AST信息。环境配置的方法有如下两种:

  1. 编译LLVM-Clang:这里由于我电脑的配置不够,在编译的时候老出现内存溢出,就放弃了。如果想自己编译LLVM-Clang的话可以参考它的官方文档,知乎上也有不少教程。
  2. 下载安装包:如果没有自定义或编译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树节点的意义如下:

节点类型意义
TranslationUnitDeclClang AST的顶层节点,遍历AST实际上就是对TranslationUnitDecl的子节点进行遍历
CompoundStmt代码块,函数实现、struct、enum、for的body一般会用它包起来
DeclStmt定义语句,VarDecl等类型的定义一般会用它包起来
VarDecl变量定义语句
MethodDecl函数定义语句
FieldDecl成员变量定义语句
IfStmtif语句,包括Cond、TrueBody、FalseBody三部分。
ForStmtfor语句
UnaryOperator一元操作符
BinaryOperator二元操作符,包括=、>、<、<=、>=、==等各种二元操作
ImplicitCastExpr隐式转换表达式
CallExpr函数调用表达式
ReturnStmt函数返回语句
ParenExpr括号表达式
TypedefDecl类型转换语句,如遇指针类型会内建PointerType和BuiltinType实现转换
RecordDeclclass或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:

参考资料

  1. 使用 Clang 工具自由的支配 C++ 代码吧 · ykiko’s blog
  2. 使用模板技巧和LibClang实现简易C++静态反射系统 - 知乎 (zhihu.com)
  3. BoomingTech/Piccolo: Piccolo (formerly Pilot) – mini game engine for games104 (github.com)