溺的文档
Dumper-7 · 第 3 篇 / 共 4 篇

Dumper-7 数据收集与管理层

2026-06-30 · 阅读 3

本文是 Dumper-7 解读系列的第三篇,承接:

本文讲解 Generator::InitInternal() 阶段——在所有偏移和名称解析就绪之后、正式生成代码之前,如何把散落在 GObjects 中的反射对象收集、组织、去重、排序成生成器可以直接消费的结构化数据

目录

  1. 概述:InitInternal 在管线中的位置
  2. 数据流总览
  3. HashStringTable:名称去重的基石
  4. PackageManager:包组织与循环依赖打破 ⭐
  5. StructManager:大小、对齐与尾部填充
  6. EnumManager:底层类型推断与枚举值冲突
  7. CollisionManager:名称冲突检测与重命名 ⭐
  8. MemberManager:成员归并迭代
  9. DependencyManager:依赖拓扑排序
  10. Wrapper 抽象层:统一真实成员与预定义成员
  11. PredefinedMembers:预定义成员机制
  12. 关键数据结构索引

1. 概述:InitInternal 在管线中的位置

Generator::InitInternal() 是 SDK 生成的第二大阶段,位于偏移推断之后、代码生成之前:

void Generator::InitInternal()
{
    // 收集所有包、包内的结构体/类/枚举/函数及包间依赖
    PackageManager::Init();

    // 收集所有结构体及其名称、大小、对齐
    StructManager::Init();

    // 收集所有枚举及其名称、底层类型
    EnumManager::Init();

    // 初始化所有成员名称冲突
    MemberManager::Init();

    // 在 StructManager 就绪后做后处理——检测并打破循环依赖
    PackageManager::PostInit();
}

这一层由 6 个 Manager + 若干支撑设施组成,各自职责如下:

组件 职责
PackageManager 按"包(Package)"组织所有反射对象;建立包间依赖关系;检测并打破包级循环依赖
StructManager 收集所有 UStruct/UClass,计算对齐、未对齐大小、是否 final、是否重用父类尾部填充
EnumManager 收集所有 UEnum,推断底层整型大小与符号性;检测枚举值名称冲突;标记非法名称
MemberManager 提供成员/函数的归并迭代器,把真实反射成员与预定义成员透明合并;管理保留名
CollisionManager 精细检测成员/函数/参数的名称冲突类型,计算重命名后缀
DependencyManager 对结构体/类做 DFS 后序拓扑排序,决定生成顺序
HashStringTable 高效的字符串去重表,所有名称冲突检测的底层基石

设计哲学:直接从内存读对象是"无结构、易越界、慢"的;Manager 层把这些原始读取一次性缓存成索引化的元信息StructInfoEnumInfoPackageInfoNameInfo),生成器随后只与这些缓存交互。这既提速,又把"版本差异 / 名称冲突 / 内存布局"等脏活集中在一处处理。


2. 数据流总览

┌──────────────────────────────────────────────────────────────────┐
│  GObjects(运行时内存中的全部 UObject)                            │
└───────────────────────────────┬──────────────────────────────────┘
                                 │  遍历 + 分类
                                 ▼
┌──────────────────────────────────────────────────────────────────┐
│                       InitInternal 数据收集层                      │
│                                                                    │
│   PackageManager ── 按包分组、建立依赖图、打破循环依赖              │
│        │                                                           │
│        ├── StructManager ── StructInfo{大小,对齐,final,尾部填充}    │
│        ├── EnumManager   ── EnumInfo{底层类型,值冲突}               │
│        ├── MemberManager ── 归并迭代器 + 保留名                     │
│        │       └── CollisionManager ── NameInfo{冲突计数}           │
│        └── DependencyManager ── 拓扑排序(生成顺序)               │
│                                                                    │
│   全程依赖 ► HashStringTable(名称去重 + 唯一性标志)              │
└───────────────────────────────┬──────────────────────────────────┘
                                 │  通过 Wrapper 抽象层暴露
                                 ▼
┌──────────────────────────────────────────────────────────────────┐
│   StructWrapper / PropertyWrapper / FunctionWrapper / EnumWrapper  │
│   (统一"真实反射成员"与"预定义成员"两种来源)                     │
└───────────────────────────────┬──────────────────────────────────┘
                                 │
                                 ▼
                    CppGenerator / MappingGenerator / ...

3. HashStringTable:名称去重的基石

HashStringTable 是一个为"海量短字符串去重 + 唯一性标记 + 稳定索引"量身定制的数据结构。Manager 层用它来存储所有的包名、类名、成员名、枚举值名,并据此判断哪些名字发生了冲突。

3.1 SmallPearsonHash:5 位皮尔逊哈希

inline uint8 SmallPearsonHash(const char* StringToHash)
{
    uint8 Hash = 0;
    while (*StringToHash != '\0')
    {
        const uint8 MaskedDownChar = (*StringToHash - 'A') & HashMask;  // 5 位掩码
        Hash = FiveBitPermutation[Hash ^ MaskedDownChar];
        StringToHash++;
    }
    return (Hash & HashMask);   // 0~31
}

哈希值只有 5 位(0~31),因此整个表只需 32 个桶(bucket)FiveBitPermutation 是一张 32 项的置换表,提供位置无关性,降低碰撞。

3.2 StringEntry:紧凑的字符串条目

#pragma pack(push, 0x1)
class StringEntry
{
    uint16 Length : 11;               // 长度(最多 2048)
    uint16 Hash : 5;                  // 5 位哈希
    uint8  bIsWide : 1;               // 宽字符标志

    mutable uint8 bIsUnique : 1;      // 在表中是否唯一
    mutable uint8 bIsUniqueTemp : 1;  // 临时唯一标记
    mutable uint8 OptionalCollisionCount : 5;  // 可选碰撞计数

    union { char Char[2048]; wchar_t WChar[2048]; };  // 非 null 结尾
};

bIsUnique 是后续命名冲突判断的关键:当同一个字符串被第二次 FindOrAdd 且要求标记重复时,该位被清零。

3.3 HashStringTableIndex:32 位紧凑索引

struct HashStringTableIndex
{
    static constexpr int32 InvalidIndex = -1;

    uint32 Unused : 1;
    uint32 HashIndex : 5;        // 桶号(0~31)
    uint32 InBucketOffset : 26;  // 桶内字节偏移(最大约 64M)

    operator int32() const;      // 可隐式转 int32,便于与 -1 比较
};

一个名称在表中的位置可压缩成单个 uint32:5 位定位桶,26 位定位桶内偏移。这样各 Manager 的 *Info 结构里只需用一个整型字段引用名字。

3.4 FindOrAdd 的语义

// 返回 {索引, 是否为新插入}
auto [Index, bWasInserted] = Table.FindOrAdd(Name, /*bShouldMarkAsDuplicated=*/true);
  • bWasInserted == true:首次出现,名字唯一。
  • bWasInserted == false:已存在;若 bShouldMarkAsDuplicated,则把该条目的 bIsUnique 清零、OptionalCollisionCount++

各 Manager 正是通过这个返回值,第一时间知道"这个名字撞了没有"。


4. PackageManager:包组织与循环依赖打破 ⭐

PackageManager(745 行)是 InitInternal 中最复杂的部分。它把 UE 的"包"概念映射到生成的文件,并解决 C++ 头文件无法处理的循环 #include 问题。

4.1 包(Package)与生成文件的映射

UE 中每个 UObject 都属于某个顶层包(通过 GetPackageIndex() 获取)。PackageManager 把同一个包里的所有反射对象聚到一起,每个非空包最终对应最多四个文件:

文件 内容
{Package}_structs.hpp 结构体(UScriptStruct)与枚举
{Package}_classes.hpp 类(UClass)定义与成员函数声明
{Package}_parameters.hpp 函数参数结构体
{Package}_functions.cpp 函数实现(通过 ProcessEvent 调用)

4.2 InitDependencies:收集依赖

PackageManager::InitDependencies() 遍历 GObjects,对每个结构体/类做三件事:

  1. 收集成员引用的类型——PackageManagerUtils::GetDependencies() 递归遍历每个属性,找出它引用的结构体/枚举/类:
std::unordered_set<int32> GetDependencies(UEStruct Struct, int32 StructIndex)
{
    std::unordered_set<int32> Dependencies;
    for (UEProperty Property : Struct.GetProperties())
        GetPropertyDependency(Property, Dependencies);  // 递归
    Dependencies.erase(StructIndex);   // 移除自引用
    return Dependencies;
}

递归会下钻进容器与委托:ArrayProperty/SetProperty 取元素类型,MapProperty 取键值类型,OptionalProperty 取值类型,DelegateProperty/MulticastInlineDelegateProperty 遍历签名函数的参数。

  1. 处理继承——若父类在别的包,记为包依赖;若在同包,记为"同包内排序依赖"(交给 DependencyManager)。

  2. 处理函数参数依赖——类的每个函数的参数类型也会引入包依赖(写入 ParametersDependencies)。

依赖被分成三类,分别对应三种文件的 #include 需求:

struct DependencyInfo
{
    DependencyListType StructsDependencies;    // _structs.hpp 需要的包
    DependencyListType ClassesDependencies;    // _classes.hpp 需要的包
    DependencyListType ParametersDependencies; // _parameters.hpp 需要的包
};

struct RequirementInfo
{
    int32 PackageIdx;
    bool bShouldIncludeStructs;  // 是否 #include "X_structs.hpp"
    bool bShouldIncludeClasses;  // 是否 #include "X_classes.hpp"
};

4.3 InitNames:包名去重

InitNames()HashStringTable 给每个包名去重,重名的包用序号区分(PackageName_0.hppPackageName_1.hpp):

auto [Name, bWasInserted] = UniquePackageNameTable.FindOrAdd(PackageName);
Info.Name = Name;
if (!bWasInserted) [[unlikely]]
    Info.CollisionCount = UniquePackageNameTable[Name].GetCollisionCount().CollisionCount;

4.4 PostInit / HandleCycles:打破循环依赖 ⭐

这是 PackageManager 的精华。当两个包互相依赖时:

PackageA_structs.hpp  ──#include──►  PackageB_structs.hpp
       ▲                                     │
       └──────────────#include───────────────┘   ← 死循环!

C++ 的 #pragma once 无法解决这种相互包含。Dumper-7 的处理算法分五步:

第一步:DFS 检测环

沿依赖图深度优先遍历,用 VisitedNodes 记录递归栈中的包。若再次进入栈中已有的包,即发现一个环,触发 OnFoundCycle 回调。

第二步:判断是否双向依赖

bool bIsMutualInclusion =
    CurrentPackageInfo.GetPackageDependencies().StructsDependencies.contains(PreviousPackageIndex)
 && PreviousPackageInfo.GetPackageDependencies().StructsDependencies.contains(CurrentPackageIndex);

第三步:统计依赖数量,决定从谁身上"断边"

分别统计"当前包需要对方多少个结构体"和"对方需要当前包多少个结构体",从依赖较少的一方删除这条依赖边——因为删除依赖意味着要改用前向声明,删掉依赖更少的那条代价最小:

const bool bCurrentHasMoreDependencies = NumStructsRequiredByCurrent > NumStructsRequiredByPrevious;
const int32 PackageToBreakCycle = bCurrentHasMoreDependencies ? PreviousPackageIndex : CurrentPackageIndex;

第四步:标记被断边包里的结构体为"循环包成员"

对被选中断边的包,遍历其所有结构体,调用 StructManager::PackageManagerSetCycleForStruct(),把它们标记 bIsPartOfCyclicPackage = true。生成器看到这个标记后,会对相关成员改用特殊的"循环修复类型"(如以字节数组占位或前向声明指针)。

第五步:补充枚举前向声明

结构体不能前向声明,但枚举可以。断边后,被影响的包需要为来自循环对方的枚举补上前向声明:

// 遍历结构体成员,找出引用了"循环对方包"中枚举的属性,登记前向声明
for (UEProperty Child : Struct.GetProperties())
    if (Child.IsA(EClassCastFlags::EnumProperty))
    {
        UEEnum Enum = Child.Cast<UEEnumProperty>().GetEnum();
        if (Enum.GetPackageIndex() == RequiredPackageIdx)
            EnumForwardDeclarations.emplace_back(Enum.GetIndex(), bMarkAsClass);
    }

最后从依赖图中真正移除这条边。注意一个优化:类(class)之间的循环可以只关闭 class 的包含、保留 struct 的包含(因为类成员大多是指针,可前向声明),从而尽量减少要改写的内容。

struct PackageInfo
{
    int32 PackageIndex;
    HashStringTableIndex Name;
    uint64 CollisionCount;

    DependencyManager StructsSorted;   // 同包内结构体的拓扑排序
    DependencyManager ClassesSorted;   // 同包内类的拓扑排序

    std::vector<int32> Functions;
    std::vector<int32> Enums;
    std::vector<std::pair<int32, bool>> EnumForwardDeclarations;  // 循环时的枚举前向声明

    DependencyInfo PackageDependencies;
};

5. StructManager:大小、对齐与尾部填充

StructManager(271 行)为每个结构体/类计算生成代码所需的精确布局信息,存入 StructInfo

struct StructInfo
{
    HashStringTableIndex Name;
    int32 LastMemberEnd = 0x0;        // 最后一个成员的结束偏移
    int32 Size = INT_MAX;             // 未对齐大小
    int32 Alignment = 0x1;            // 对齐要求

    bool bUseExplicitAlignment;       // 是否需要 alignas() 修饰
    bool bHasReusedTrailingPadding;   // 子类是否重用了父类的尾部填充
    bool bIsFinal = true;             // 是否为最终类(无子类)
    bool bIsPartOfCyclicPackage;      // 所在包是否有循环依赖
};

5.1 InitAlignmentsAndNames:两遍计算对齐

第一遍:每个结构体的对齐 = max(MinAlignment, 所有成员对齐的最大值)。其中:

  • 接口类(Interface)被特殊置为大小 0、对齐 1;
  • 有父类的 class 至少使用指针对齐(64 位下 0x8)。
  • bUseExplicitAlignment = (MinAlignment > 最大成员对齐)——只有当结构体声明的最小对齐超过成员自然对齐时,才需要在生成代码里写 alignas

第二遍:沿继承链从顶层父类向子类传播对齐——子类对齐不得小于父类。若子类对齐由父类继承而来,则置 bUseExplicitAlignment = false,避免冗余的 alignas

5.2 InitSizesAndIsFinal:尾部填充重用

bHasReusedTrailingPadding 对应 C++ 编译器的一个微妙行为:子类的首个成员可以占用父类尾部的对齐填充空间

struct Parent {          // sizeof 对齐到 0x8,但实际数据只到 0x1
    uint8 DeltaFlags;    // 0x0
    // 隐式填充 [0x1..0x7]
};
struct Child : Parent {
    uint8 Extra[0x7];    // 偏移 0x1 —— 重用了 Parent 的尾部填充!
};

判定逻辑:若 Align(父类大小, 父类对齐) > 子类最低成员偏移,说明发生了重用,于是把父类大小回调到该最低偏移,并置 bHasReusedTrailingPadding = true

if (Align(SizeToCheck, SuperInfo.Alignment) > LowestOffset)
{
    if (SuperInfo.Size > LowestOffset)
        SuperInfo.Size = LowestOffset;
    SuperInfo.bHasReusedTrailingPadding = true;
}

生成器据此会对这种结构加 #pragma pack(push, 0x1),否则编译器算出的 sizeof/offsetof 会与游戏内存不一致。

遍历继承链时还会把所有父类标记为 bIsFinal = false——没有被任何子类继承的类才是 final(配合 Settings::CppGenerator::bAddFinalSpecifier,给它们加 final 关键字)。

StructInfoHandle 暴露三个核心查询:

方法 返回
GetSize() Align(Size, Alignment)(对齐后大小)
GetUnalignedSize() Size(未对齐大小,用于计算尾部填充)
GetAlignment() Alignment

6. EnumManager:底层类型推断与枚举值冲突

EnumManager(252 行)解决两个问题:枚举的底层整型类型,以及枚举值的名称冲突。

struct EnumInfo
{
    HashStringTableIndex Name;
    uint8 UnderlyingTypeSize = 0x1;   // 1/2/4/8 字节
    bool bIsSigned = false;
    bool bWasInstanceFound = false;
    std::vector<EnumCollisionInfo> MemberInfos;
};

struct EnumCollisionInfo
{
    HashStringTableIndex MemberName;
    uint64 MemberValue;
    uint8 CollisionCount = 0;
    // 最终名 = MemberName + (CollisionCount > 0 ? "_" + (CollisionCount - 1) : "")
};

6.1 底层类型的三级推断

引擎在不同版本里对枚举底层类型的暴露程度不同,于是有三种递进的推断:

  1. 直接读取——若 bHasUnderlayingTypeInUEnum(引擎的 UEnum 自带底层类型字段),调用 GetSizeSignedPair() 直接得到大小与符号性。
  2. 从使用处推断——否则遍历所有属性,找到引用该枚举的 EnumProperty/ByteProperty,取其属性大小作为枚举大小:UnderlyingTypeSize = max(已知, Property.GetSize())
  3. 按最大值推断——若既无字段又无使用处,遍历枚举值(忽略 _MAX 项)取最大值,用能容纳它的最小整型:
void SetEnumSizeForValue(uint8& Size, uint64 EnumValue)
{
    if      (EnumValue > 0xFFFFFFFF) Size = 0x8;            // uint64
    else if (EnumValue > 0xFFFF)     Size = max(Size, 0x4); // uint32
    else if (EnumValue > 0xFF)       Size = max(Size, 0x2); // uint16
    else                             Size = max(Size, 0x1); // uint8
}

6.2 枚举值名称冲突与非法名

InitIllegalNames() 预先登记一批不能直接作枚举值名的标识符(C++ 关键字与冲突宏):

IllegalNames = { "IN", "OUT", "TRUE", "FALSE", "DELETE", "PF_MAX", "int", "short", "long", ... };

收集每个枚举值时,用全局表 UniqueEnumValueNames.FindOrAdd 判重,并区分两种冲突:

  • 全局冲突(不同枚举里有同名值);
  • 局部冲突(同一个枚举里出现两个同名值,常见于重复定义)。

只有局部冲突或撞上非法名才需要加后缀:

// 局部重复:在本枚举先前的值里找到同名 → CollisionCount = 前者 + 1
// 撞非法名:CollisionCount++
// 输出:Value、Value_0、Value_1 ...

7. CollisionManager:名称冲突检测与重命名 ⭐

CollisionManager(347 行)处理 C++ 代码生成里最棘手的问题:同一作用域里不能有重名的成员/函数/参数。它要识别冲突来自哪个层级,并据此选择重命名策略。

7.1 冲突类型与紧凑计数

enum class ECollisionType : uint8
{
    MemberName,       // 结构体自身成员
    SuperMemberName,  // 继承自父类的成员
    FunctionName,     // 自身函数
    SuperFunctionName,// 继承自父类的函数
    ParameterName,    // 函数参数
    None,
};

每个名字的冲突信息压缩进一个 32 位整数 NameInfo

struct NameInfo
{
    HashStringTableIndex Name;
    union
    {
        struct
        {
            uint32 OwnType : 7;                       // 本元素类型(ECollisionType)
            uint32 MemberNameCollisionCount : 5;      // 与本类成员的冲突数
            uint32 SuperMemberNameCollisionCount : 5; // 与父类成员的冲突数
            uint32 FunctionNameCollisionCount : 5;    // 与本类函数的冲突数
            uint32 SuperFuncNameCollisionCount : 5;   // 与父类函数的冲突数
            uint32 ParamNameCollisionCount : 5;       // 与同函数其它参数的冲突数
        };
        uint32 CollisionData;
    };
};

用一个 uint32 同时编码"我是谁"和"我和 5 类东西各撞了几次"——非常节省内存,且 5 位计数(0~31)对单个作用域内的重名次数绰绰有余。

7.2 多层级冲突检测

AddNameToContainer() 按优先级在多个作用域中查找同名项(反向查找最近的一个),命中即累加相应计数:

检测顺序(命中即返回):
1. (参数)与同函数内其它参数冲突?
2. 与本结构体内其它成员冲突?
3. 沿父类链,与任一父类的成员/函数冲突?(标记为 Super*)
4. 与类的预定义保留名(如 StaticClass、Flags)冲突?
5. 与全局保留字(C++ 关键字)冲突?
6. 都没撞 → 直接登记

7.3 StringifyName:重命名规则

StringifyName() 根据 NameInfo 把名字翻译成最终的、保证不撞的标识符:

// 成员名
if (OwnType == MemberName)
{
    if (SuperMemberNameCollisionCount > 0) Name += "_" + Struct.GetValidName();   // 与父类成员撞 → 加类名后缀
    if (MemberNameCollisionCount > 0)      Name += "_" + (MemberNameCollisionCount - 1);  // 与本类成员撞 → 加序号
}
// 函数名
else if (OwnType == FunctionName)
{
    if (MemberNameCollisionCount > 0 || SuperMemberNameCollisionCount > 0)
        Name = "Func_" + Name;   // 与成员撞 → 加 Func_ 前缀
    if (FunctionNameCollisionCount > 0)
        Name += "_" + (FunctionNameCollisionCount - 1);
}
// 参数名
else if (OwnType == ParameterName)
{
    if (任意成员/函数冲突)  Name = "Param_" + Name;   // 与成员/函数撞 → 加 Param_ 前缀
    if (ParamNameCollisionCount > 0)
        Name += "_" + (ParamNameCollisionCount - 1);
}

效果示例:

场景 原名 重命名后
子类成员与父类成员同名 Child::X(父类也有 X X_Child
同结构体内两个同名成员 X, X X, X_0
函数与成员同名 int Bar; void Bar(); Bar, Func_Bar
参数撞上 UObject 内置成员 void F(int Flags) void F(int Param_Flags)

7.4 ResolveEffectiveNameConflicts:二次冲突

加了前缀/后缀后,新名字本身可能又撞上别的名字。例如成员 Params 撞保留字被改成 Params_0,恰好和另一个本就叫 Params_0 的成员冲突。ResolveEffectiveNameConflicts() 会循环调用 StringifyName 比对最终输出名,发现仍冲突就继续递增计数,直到唯一(或达到 5 位计数上限)。


8. MemberManager:成员归并迭代

MemberManager(215 行)是生成器读取一个结构体成员/函数的统一入口。它做两件事:把真实反射成员与预定义成员按规则归并迭代,以及托管 CollisionManager 所需的保留名。

8.1 构造时排序并挂接预定义

MemberManager::MemberManager(UEStruct Str)
    : Struct(std::make_shared<StructWrapper>(Str))
    , Functions(Str.GetFunctions())
    , Members(Str.GetProperties())
{
    std::sort(Functions.begin(), Functions.end(), CompareUnrealFunctions);
    std::sort(Members.begin(),   Members.end(),   CompareUnrealProperties);

    // 查表:本结构体是否有预定义成员/函数
    if (PredefinedMemberLookup)
    {
        auto It = PredefinedMemberLookup->find(Struct->GetUnrealStruct().GetIndex());
        if (It != PredefinedMemberLookup->end())
        {
            PredefMembers   = &It->second.Members;
            PredefFunctions = &It->second.Functions;
        }
    }
}

CompareUnrealProperties 按偏移排序(位域在同偏移下按 BitIndex 排序);CompareUnrealFunctions 把静态函数排前、const 函数排后。

8.2 双指针归并迭代

MemberIterator 同时持有真实成员数组与预定义成员数组两个游标,每次取偏移更小的那个,从而把预定义成员透明地插入到真实成员的偏移序列中:

const int32 NextUnrealOffset = GetUnrealMemberOffset();   // 末尾返回 0xFFFFFFF
const int32 NextPredefOffset = GetPredefMemberOffset();
bIsCurrentlyPredefined = NextPredefOffset < NextUnrealOffset;

FunctionIterator 类似,但排序规则更复杂:非内联静态预定义函数排最前,内联函数体的预定义函数排最后(因为它们直接写在类定义里)。

这种设计让生成器完全不必区分一个成员是"从内存读到的真实属性"还是"手写补充的预定义成员"——它只管按顺序迭代。统一的关键正是下一节的 Wrapper 层。

8.3 保留名

InitReservedNames() 登记三类保留名,交给 CollisionManager 参与冲突检测:

  • UObject 内置成员FlagsIndexClassNameOuter
  • 函数体局部变量FuncParmsParamsFlgs(生成的 ProcessEvent 桩里会用到这些局部变量名,成员若同名会编译失败);
  • C++ 关键字与类型intfloat 等 170 余项。

9. DependencyManager:依赖拓扑排序

DependencyManager(60 行)是一个轻量的 DFS 后序遍历器,用于决定"同一个包内的结构体/类按什么顺序生成"——被依赖者必须先定义。

struct IndexDependencyInfo
{
    mutable uint64 IterationHitCounter = 0x0;     // 防重复访问
    std::unordered_set<int32> DependencyIndices;  // 本节点依赖谁
};

核心是后序递归:先访问所有依赖,再回调自身——于是回调顺序天然是拓扑序:

void VisitIndexAndDependencies(int32 Index, OnVisitCallbackType Callback) const
{
    auto& [HitCounter, Dependencies] = AllDependencies.at(Index);
    if (HitCounter >= CurrentIterationHitCount)  // 已访问,跳过
        return;
    HitCounter = CurrentIterationHitCount;

    for (int32 Dependency : Dependencies)
        VisitIndexAndDependencies(Dependency, Callback);   // 先依赖

    Callback(Index);   // 后自身 —— 保证依赖先被生成
}

IterationHitCounter 与全局 CurrentIterationHitCount 配合,使得同一个 DependencyManager 可以被多次完整遍历而无需清理"已访问"标记(每次遍历前递增全局计数即可)。

生成器对每个包调用 Package.GetSortedStructs().VisitAllNodesWithCallback(生成回调),即按正确顺序逐个生成结构体。


10. Wrapper 抽象层:统一真实成员与预定义成员

生成器面对的数据有两种来源:从内存读出的真实反射对象(UEStruct/UEProperty/UEFunction/UEEnum),以及手写补充的预定义成员(PredefinedStruct/PredefinedMember/PredefinedFunction)。Wrapper 层用 union 把两者统一到同一接口下,让生成器无需区分。

10.1 StructWrapper

union {
    const UEStruct Struct;                  // 真实结构
    const PredefinedStruct* PredefStruct;   // 预定义结构
};
StructInfoHandle InfoHandle;                // 来自 StructManager 的缓存
bool bIsUnrealStruct = false;               // 标记当前来源

无论来源如何,生成器统一调用同一组接口:

接口 说明
GetName() / GetUniqueName() 名称(后者返回 {名称, 是否唯一}
GetSuper() 父结构(返回 StructWrapper,可递归)
GetMembers() 返回 MemberManager,归并迭代成员与函数
GetSize() / GetAlignment() / GetUnalignedSize() 真实结构走 InfoHandle,预定义走 PredefStruct
IsClass() / IsUnion() / IsInterface() 类型判定
ShouldUseExplicitAlignment() / HasReusedTrailingPadding() 影响生成代码的 alignas / #pragma pack
IsCyclicWithPackage() 是否与某包构成循环(影响成员类型修复)

10.2 PropertyWrapper / FunctionWrapper

// PropertyWrapper
union { const UEProperty Property; const PredefinedMember* PredefProperty; };
NameInfo Name;                  // 真实属性才需要的冲突信息
bool bIsUnrealProperty = false;

PropertyWrapper 统一暴露 GetName/GetOffset/GetSize/GetArrayDim/IsBitField/GetBitIndex/...。其中 GetType() 只对预定义属性有效(预定义直接给出类型字符串);真实属性的类型字符串由生成器另行通过 Cast<UEXxxProperty>() 推导(见代码生成文档的"类型映射")。

FunctionWrapper 同理统一真实函数与预定义函数,并提供 AsStruct() 把函数的参数视为一个伪结构体来迭代——这正是参数结构体 _params 的生成基础。

10.3 EnumWrapper

枚举没有预定义机制,所以 EnumWrapper 只封装真实 UEEnum + EnumInfoHandle,提供 GetUnderlyingTypeSize/IsUnderlyingTypeSigned/GetNumMembers/GetMembers。枚举值通过 CollisionInfoIterator 返回,自带冲突信息。


11. PredefinedMembers:预定义成员机制

有些成员/方法无法从反射系统自动获得——例如 UObjectVTable 指针、GetName()/IsA() 等手写方法、ULevel::Actors 这种引擎内部数组。PredefinedMembers.h 提供"预定义"机制手工补齐它们。

11.1 三个核心结构

struct PredefinedMember
{
    std::string Comment, Type, Name;
    int32 Offset, Size, ArrayDim, Alignment;
    bool bIsStatic, bIsZeroSizeMember;
    bool bIsBitField; uint8 BitIndex, BitCount;
    std::string DefaultValue;
};

struct PredefinedFunction
{
    std::string CustomComment, CustomTemplateText;
    std::string ReturnType, NameWithParams, NameWithParamsWithoutDefaults;
    std::string Body;
    bool bIsStatic, bIsConst, bIsBodyInline;   // bIsBodyInline 决定写在 .h 还是 .cpp
};

struct PredefinedStruct
{
    std::string CustomTemplateText, UniqueName;
    int32 Size, Alignment;
    bool bUseExplictAlignment, bIsFinal, bIsClass, bIsUnion;
    const PredefinedStruct* Super;
    std::vector<PredefinedMember> Properties;
    std::vector<PredefinedFunction> Functions;
};

11.2 注册与查找

预定义成员存放在一张以结构体索引为键的表中,由各生成器在 InitPredefinedMembers() / InitPredefinedFunctions() 填充:

struct PredefinedElements
{
    std::vector<PredefinedMember> Members;
    std::vector<PredefinedFunction> Functions;
};

using PredefinedMemberLookupMapType = std::unordered_map<int32 /*StructIndex*/, PredefinedElements>;

Generator::Generate<T>() 在生成前调用 MemberManager::SetPredefinedMemberLookupPtr(&T::PredefinedMembers),把该生成器的预定义表挂到全局;之后 MemberManager 构造时按结构体索引查表,并通过归并迭代器把预定义成员插入到真实成员序列里(见第 8 节)。

CppGenerator 正是用这套机制给 UObject 注入 VTable/Flags/Index/Class/Name/Outer 等成员,以及 GetName()/GetFullName()/IsA()/StaticClass()/ProcessEvent() 等方法——详见代码生成文档第 7 节。

11.3 排序规则

PredefinedMembers.h 还定义了成员/函数的排序比较函数,确保生成代码顺序稳定:

  • 成员:实例成员按偏移排序在前,静态成员按名称排序在后;
  • 函数:非内联在前(写进 .h 的 public 区),内联在后;静态在前,const 在后。

12. 关键数据结构索引

结构 所属 关键字段
PackageInfo PackageManager NameStructsSortedClassesSortedFunctionsEnumsEnumForwardDeclarationsPackageDependencies
DependencyInfo / RequirementInfo PackageManager 三类包依赖列表 + 单条依赖的 include 需求
StructInfo StructManager SizeAlignmentLastMemberEndbUseExplicitAlignmentbHasReusedTrailingPaddingbIsFinalbIsPartOfCyclicPackage
EnumInfo / EnumCollisionInfo EnumManager UnderlyingTypeSizebIsSignedMemberInfos / MemberNameMemberValueCollisionCount
NameInfo / ECollisionType CollisionManager 32 位压缩的 OwnType + 5 类冲突计数
IndexDependencyInfo DependencyManager IterationHitCounterDependencyIndices
StringEntry / HashStringTableIndex HashStringTable 长度/哈希/唯一性位 / 桶号+桶内偏移
PredefinedMember/Function/Struct PredefinedMembers 手写补充的成员/方法/结构定义

文档版本: 1.0 最后更新: 2026-06-30 基于 Dumper-7 源码分析(Managers / Wrappers / HashStringTable / PredefinedMembers)

评论

  • 还没有评论,来说点什么吧。

无需注册或登录,填个昵称即可评论。