Dumper-7 数据收集与管理层
本文是 Dumper-7 解读系列的第三篇,承接:
- Dumper-7 偏移推断算法:偏移推断(
InitEngineCore的偏移部分)- Dumper-7 FName 名称系统:FName 名称解析
本文讲解
Generator::InitInternal()阶段——在所有偏移和名称解析就绪之后、正式生成代码之前,如何把散落在 GObjects 中的反射对象收集、组织、去重、排序成生成器可以直接消费的结构化数据。
目录
- 概述:InitInternal 在管线中的位置
- 数据流总览
- HashStringTable:名称去重的基石
- PackageManager:包组织与循环依赖打破 ⭐
- StructManager:大小、对齐与尾部填充
- EnumManager:底层类型推断与枚举值冲突
- CollisionManager:名称冲突检测与重命名 ⭐
- MemberManager:成员归并迭代
- DependencyManager:依赖拓扑排序
- Wrapper 抽象层:统一真实成员与预定义成员
- PredefinedMembers:预定义成员机制
- 关键数据结构索引
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 层把这些原始读取一次性缓存成索引化的元信息(
StructInfo、EnumInfo、PackageInfo、NameInfo),生成器随后只与这些缓存交互。这既提速,又把"版本差异 / 名称冲突 / 内存布局"等脏活集中在一处处理。
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,对每个结构体/类做三件事:
- 收集成员引用的类型——
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 遍历签名函数的参数。
处理继承——若父类在别的包,记为包依赖;若在同包,记为"同包内排序依赖"(交给
DependencyManager)。处理函数参数依赖——类的每个函数的参数类型也会引入包依赖(写入
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.hpp、PackageName_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 底层类型的三级推断
引擎在不同版本里对枚举底层类型的暴露程度不同,于是有三种递进的推断:
- 直接读取——若
bHasUnderlayingTypeInUEnum(引擎的UEnum自带底层类型字段),调用GetSizeSignedPair()直接得到大小与符号性。 - 从使用处推断——否则遍历所有属性,找到引用该枚举的
EnumProperty/ByteProperty,取其属性大小作为枚举大小:UnderlyingTypeSize = max(已知, Property.GetSize())。 - 按最大值推断——若既无字段又无使用处,遍历枚举值(忽略
_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 内置成员:
Flags、Index、Class、Name、Outer; - 函数体局部变量:
Func、Parms、Params、Flgs(生成的 ProcessEvent 桩里会用到这些局部变量名,成员若同名会编译失败); - C++ 关键字与类型:
int、float等 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:预定义成员机制
有些成员/方法无法从反射系统自动获得——例如 UObject 的 VTable 指针、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 | Name、StructsSorted、ClassesSorted、Functions、Enums、EnumForwardDeclarations、PackageDependencies |
DependencyInfo / RequirementInfo |
PackageManager | 三类包依赖列表 + 单条依赖的 include 需求 |
StructInfo |
StructManager | Size、Alignment、LastMemberEnd、bUseExplicitAlignment、bHasReusedTrailingPadding、bIsFinal、bIsPartOfCyclicPackage |
EnumInfo / EnumCollisionInfo |
EnumManager | UnderlyingTypeSize、bIsSigned、MemberInfos / MemberName、MemberValue、CollisionCount |
NameInfo / ECollisionType |
CollisionManager | 32 位压缩的 OwnType + 5 类冲突计数 |
IndexDependencyInfo |
DependencyManager | IterationHitCounter、DependencyIndices |
StringEntry / HashStringTableIndex |
HashStringTable | 长度/哈希/唯一性位 / 桶号+桶内偏移 |
PredefinedMember/Function/Struct |
PredefinedMembers | 手写补充的成员/方法/结构定义 |
文档版本: 1.0 最后更新: 2026-06-30 基于 Dumper-7 源码分析(Managers / Wrappers / HashStringTable / PredefinedMembers)
评论
- 还没有评论,来说点什么吧。