WITH_EDITOR 包裹反射属性的问题

有时只想要一些属性在编辑器下存在,打包时不需要,按照常规的思路,需要对这些属性使用 WITH_EDITOR 包裹:

1
2
3
4
#if WITH_EDITOR
UPROPERTY()
int32 ival;
#endif

这个代码在 Editor 的 Configuration 下没有问题,但是一旦编译非 Editor 就会产生如下错误:

1
ERROR: Build/Win64/FGame/Inc/FGame/NetActor.gen.cpp(97): error C2039: 'ival': is not a member of 'ANetActor'

那么,既然我们明明已经用 WITH_EDITOR 包裹了 ival 的属性,为什么在编译非 Editor 的时候 UHT 还会为这个属性生成反射代码呢?
这个问题涉及到了以下几个概念:

  1. gen.cpp 中是 UHT 为反射标记的类和属性生成的反射信息
  2. UHT 的生成流程在调用编译器之前

UE 构建系统的流程我之前做过分析:Build flow of the Unreal Engine4 project

因为 C++ 的宏是在调用编译器后预处理阶段做的事情,在执行 UHT 时,压根不会检测宏条件,所以上面的代码,UHT 依然会为 ival 生成反射信息到 gen.cpp 中,而 UHT 执行完毕之后进入编译阶段 WITH_EDITOR 会参与预处理,ival因此在类定义中不存在,但是 UHT 已经为它生成了反射代码,会通过获取成员函数指针的方式访问到它,进而产生了上述的编译错误。

所以这是 UE 反射代码生成先于预处理造成的问题,在写代码时是比较反直觉的。但是这个问题也并非不能解决,UE 提供了 WITH_EDITORONLY_DATA 宏来专门处理这个问题,一个宏解决不了,就引入一个新的。

但是为什么 WITH_EDITOR 不可以,而 WITH_EDITORONLY_DATA 就可以呢?因为 UHT 在生成反射代码时为 WITH_EDITORONLY_DATA 做了特殊检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void FNativeClassHeaderGenerator::ExportProperties(FOutputDevice& Out, UStruct* Struct, int32 TextIndent)
{
FProperty* Previous = NULL;
FProperty* PreviousNonEditorOnly = NULL;
FProperty* LastInSuper = NULL;
UStruct* InheritanceSuper = Struct->GetInheritanceSuper();

// Find last property in the lowest base class that has any properties
UStruct* CurrentSuper = InheritanceSuper;
while (LastInSuper == NULL && CurrentSuper)
{
for(TFieldIterator<FProperty> It(CurrentSuper,EFieldIteratorFlags::ExcludeSuper); It; ++It )
{
FProperty* Current = *It;

// Disregard properties with 0 size like functions.
if(It.GetStruct() == CurrentSuper && Current->ElementSize)
{
LastInSuper = Current;
}
}
// go up a layer in the hierarchy
CurrentSuper = CurrentSuper->GetSuperStruct();
}

FMacroBlockEmitter WithEditorOnlyData(Out, TEXT("WITH_EDITORONLY_DATA"));

// Iterate over all properties in this struct.
for(TFieldIterator<FProperty> It(Struct, EFieldIteratorFlags::ExcludeSuper); It; ++It )
{
FProperty* Current = *It;

// Disregard properties with 0 size like functions.
if (It.GetStruct() == Struct)
{
WithEditorOnlyData(Current->IsEditorOnlyProperty());

// Export property specifiers
// Indent code and export CPP text.
{
FUHTStringBuilder JustPropertyDecl;

const FString* Dim = GArrayDimensions.Find(Current);
Current->ExportCppDeclaration(JustPropertyDecl, EExportedDeclaration::Member, Dim ? **Dim : NULL);
ApplyAlternatePropertyExportText(*It, JustPropertyDecl, EExportingState::TypeEraseDelegates);

// Finish up line.
Out.Logf(TEXT("%s%s;\r\n"), FCString::Tab(TextIndent + 1), *JustPropertyDecl);
}

LastInSuper = NULL;
Previous = Current;
if (!Current->IsEditorOnlyProperty())
{
PreviousNonEditorOnly = Current;
}
}
}
}

看下 FMacroBlockEmitter 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct FMacroBlockEmitter
{
explicit FMacroBlockEmitter(FOutputDevice& InOutput, const TCHAR* InMacro)
: Output(InOutput)
, bEmittedIf(false)
, Macro(InMacro)
{
}

~FMacroBlockEmitter()
{
if (bEmittedIf)
{
Output.Logf(TEXT("#endif // %s\r\n"), Macro);
}
}

void operator()(bool bInBlock)
{
if (!bEmittedIf && bInBlock)
{
Output.Logf(TEXT("#if %s\r\n"), Macro);
bEmittedIf = true;
}
else if (bEmittedIf && !bInBlock)
{
Output.Logf(TEXT("#endif // %s\r\n"), Macro);
bEmittedIf = false;
}
}

FMacroBlockEmitter(const FMacroBlockEmitter&) = delete;
FMacroBlockEmitter& operator=(const FMacroBlockEmitter&) = delete;

private:
FOutputDevice& Output;
bool bEmittedIf;
const TCHAR* Macro;
};

当生成代码时会为使用 WITH_EDITORONLY_DATA 包裹的属性在 gen.cpp 中添加 WITH_EDITORONLY_DATA 宏(有点套娃的感觉),使 gen.cpp 在非 EDITOR 下编译时也不会把这部分反射代码参与真正的编译,从而解决了上面的问题。