UE4中的反射之一:编译阶段
环境
引擎版本: github 4.20
参考文章
有关反射的材料看以下几个就够了,如果你看了还不理解的话,那再看其它材料估计也没什么用,你可能需要先使用一段时间虚幻4引擎,然后再返回头来继续看
简介
UE4 中的反射大概的流程是在编译前利用 UnrealHeaderTool 对代码文件内容进行处理,生成 .generated.h/.gen.cpp 文件,在生成的代码中加入反射信息,并和自己编写的代码一起编译,在运行时动态地将这些反射信息收集起来使用。
UnrealHeaderTool 预处理
我们编写代码过程中用到的 UCLASS
UFUNCTION
UPROPERTY
这些宏就是参与这个阶段,当 UHT 处理代码时,遇到这些宏标记,就知道接下来的代码需要进行处理,具体怎么处理根据宏类型不同而不同,具体就不展开了
UCLASS
UFUNCTION
UPROPERTY
这些宏在之后的 C++ 编译器编译过程中的预处理中,都会被展开,只不过展开的内容是空的,所以这些宏只是作为 UHT 阶段的代码处理标记使用,不会影响到真正的编译
C++编译器编译代码预处理阶段
这个阶段对我们来说最重要的就是理解 GENERATED_BODY/GENERATED_USTRUCT_BODY/GENERATED_UCLASS_BODY/GENERATED_UINTERFACE_BODY/GENERATED_IINTERFACE_BODY
展开的结果
这些宏定义在 Objectmacros.h 文件中,具体定义如下
#define GENERATED_BODY_LEGACY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY_LEGACY);
#define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY);
#define GENERATED_USTRUCT_BODY(...) GENERATED_BODY()
#define GENERATED_UCLASS_BODY(...) GENERATED_BODY_LEGACY()
#define GENERATED_UINTERFACE_BODY(...) GENERATED_BODY_LEGACY()
#define GENERATED_IINTERFACE_BODY(...) GENERATED_BODY_LEGACY()
从上面定义可以看出 GENERATED_BODY/GENERATED_USTRUCT_BODY
是一致的,其他宏也是一样,最终两个关键的宏是 GENERATED_BODY/GENERATED_BODY_LEGACY
,这两个宏展开后都是由三个内容组成,第一部分是 CURRENT_FILE_ID
这个宏展开后的内容,第二部分是当前行号,第三部分是 GENERATED_BODY/GENERATED_BODY_LEGACY
字符串,三个部分间用 _
号相连,其中最重要的是第一部分 CURRENT_FILE_ID
,这个宏定义在 UHT 生成的代码 xx.generated.h 中,被定义为当前文件相对于工程根目录的路径,保证唯一。这也是为什么我们要包含 xx.generated.h 的原因,一是让 UHT 知道它需要处理这个头文件,二是因为在 C++ 编译器编译过程中需要这个文件里定义的宏
现在我们有一个类,内容如下
// ...
#include "MyActor.generated.h"
UCLASS()
class TESTCALL_API AMyActor : public AActor
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintImplementableEvent)
void MyActorBlueprintImplementableEvent();
UFUNCTION(BlueprintNativeEvent)
void MyActorBlueprintNativeEvent();
UFUNCTION(BlueprintPure)
int32 MyActorBlueprintPure();
UFUNCTION(BlueprintCallable)
void MyActorBlueprintCallable();
UFUNCTION(BlueprintGetter)
int32 MyActorBlueprintGetter();
UFUNCTION(BlueprintSetter)
void MyActorBlueprintSetter(int32 Test);
UPROPERTY()
int MyActorProperty;
};
GENERATED_BODY
宏展开的过程如下
GENERATED_BODY
-> (CURRENT_FILE_ID)_(__LINE__)_GENERATED_BODY
-> TestCall_Source_TestCall_MyActor_h_12_GENERATED_BODY
GENERATED_UCLASS_BODY
-> (CURRENT_FILE_ID)_(__LINE__)_GENERATED_BODY_LEGACY
-> TestCall_Source_TestCall_MyActor_h_12_GENERATED_BODY_LEGACY
展开到上面的结果后,得到的完整宏就定义在 MyActor.generated.h
文件中,所以上面的类定义代码就等价于
class TESTCALL_API AMyActor : public AActor
{
TestCall_Source_TestCall_MyActor_h_12_GENERATED_BODY
};
上面的结果还是一个宏,继续展开,从 MyActor.generated.h
可以看到,这个宏的定义如下
#define TestCall_Source_TestCall_MyActor_h_12_GENERATED_BODY \
PRAGMA_DISABLE_DEPRECATION_WARNINGS \
public: \
TestCall_Source_TestCall_MyActor_h_12_PRIVATE_PROPERTY_OFFSET \
TestCall_Source_TestCall_MyActor_h_12_RPC_WRAPPERS_NO_PURE_DECLS \
TestCall_Source_TestCall_MyActor_h_12_CALLBACK_WRAPPERS \
TestCall_Source_TestCall_MyActor_h_12_INCLASS_NO_PURE_DECLS \
TestCall_Source_TestCall_MyActor_h_12_ENHANCED_CONSTRUCTORS \
private: \
PRAGMA_ENABLE_DEPRECATION_WARNINGS
PRAGMA_DISABLE_DEPRECATION_WARNINGS/PRAGMA_ENABLE_DEPRECATION_WARNINGS
这两个宏是用来开启/关闭禁止编译器过期警告
TestCall_Source_TestCall_MyActor_h_12_PRIVATE_PROPERTY_OFFSET
这个宏是空内容
TestCall_Source_TestCall_MyActor_h_12_RPC_WRAPPERS_NO_PURE_DECLS
这个宏里的内容是用来展开我们之前定义的 UFUNCTION
,具体内如如下
-
UFUNCTION(BlueprintImplementableEvent)
这种函数是实现在蓝图中的,由 C++ 进行调用,所以在 .generated.h 中没有相应的实现 -
UFUNCTION(BlueprintNativeEvent)
这种函数蓝图可以进行重写,C++ 中有一份默认实现,由 C++ 进行调用,如果蓝图没有重写,那么就调用 C++ 版本的实现,可以看到在 .generated.h 中对这种函数声明了一个_Implementation
结尾的函数,所以需要在 cpp 中相应的实现_Implementation
结尾的函数。同时还对这个函数实现了一个带exec
前缀的版本,这个版本内部调用了_Implementation
后缀的版本。因为当 C++ 直接调用,或者蓝图的实现中调用 C++ 默认版本(类似 C++ 虚函数的重载时,经常会调用一下父类版本),都会先调用这个exec
版本中,最终调用到_Implementation
后缀的版本。 -
其他的
BlueprintSetter/BlueprintGetter/BlueprintCallable/BlueprintPure
,这些都会生成一个exec
前缀的版本,在这个版本的函数里直接调用了 C++ 版本
上面介绍中的 exec
版本函数是由 DECLARE_FUNCTION
这个宏来展开,这个宏定义如下
#define RESULT_PARAM Z_Param__Result
#define RESULT_DECL void*const RESULT_PARAM
#define DECLARE_FUNCTION(func) static void func( UObject* Context, FFrame& Stack, RESULT_DECL )
可见这些类成员函数最终会对应一个 exec
版本的静态成员函数,具体展开结果如下
static void execMyActorBlueprintNativeEvent( UObject* Context, FFrame& Stack, void*const Z_Param__Result )
{
Stack.Code += !!Stack.Code;
{
SCOPED_SCRIPT_NATIVE_TIMER(ScopedNativeCallTimer);
// ThisClass 是个 typedef, 是在后面的宏里定义的,在这里就是 AMyAcotr
((ThisClass*)(Context))->MyActorBlueprintNativeEvent_Implementation();
}
}
TestCall_Source_TestCall_MyActor_h_12_CALLBACK_WRAPPERS
这个宏也是个空宏
TestCall_Source_TestCall_MyActor_h_12_INCLASS
这个宏定义如下
#define TestCall_Source_TestCall_MyActor_h_12_INCLASS \
private: \
static void StaticRegisterNativesAMyActor(); \
friend struct Z_Construct_UClass_AMyActor_Statics; \
public: \
DECLARE_CLASS(AMyActor, AActor, COMPILED_IN_FLAGS(0), CASTCLASS_None, TEXT("/Script/TestCall"), NO_API) \
DECLARE_SERIALIZER(AMyActor)
这个宏也很重要,包含如下内容
声明了一个友元结构 Z_Construct_UClass_AMyActor_Statics
,之后程序运行时,会利用这个结构去收集类的反射信息
DECLARE_CLASS
宏,这个宏里需要注意的地方如下
typedef TSuperClass Super; typedef TClass ThisClass;
,这就是我们经常用的Super
定义的地方了,还有一个ThisClass
,用在之前exec
函数定义的地方inline static UClass* StaticClass()
这也是我们经常使用到的,获取一个类的反射信息对象,内部调用的是静态成员函数GetPrivateStaticClass
TestCall_Source_TestCall_MyActor_h_12_STANDARD_CONSTRUCTORS
和 TestCall_Source_TestCall_MyActor_h_12_ENHANCED_CONSTRUCTORS
这个宏里是用来生成构造函数了,这里是 GENERATED_BODY
和 GENERATED_UCLASS_BODY
唯一有区别的地方
-
GENERATED_UCLASS_BODY
->TestCall_Source_TestCall_MyActor_h_12_STANDARD_CONSTRUCTORS
这个宏里声明了一个构造函数
AMyActor(const FObjectInitializer& ObjectInitializer);
,需要在 cpp 里添加实现。还实现了一个静态函数
static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())TClass(X); }
,这个函数里就是调用了一下 placement new,在X.GetObj()
返回的内存地址上去构造当前对象,调用的是带ObjectInitializer
的构造函数 -
GENERATED_BODY
->TestCall_Source_TestCall_MyActor_h_12_ENHANCED_CONSTRUCTORS
和上面的不一样,没有声明带
ObjectInitializer
参数的构造函数,也同样实现了一个静态函数static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())TClass;}
, 只不过 placement new 调用的是不带参数的默认构造函数
还有个不一样的地方,GENERATED_UCLASS_BODY
展开的最后是 public:
,而 GENERATED_UCLASS
展开的最后是 private:
,所以在编写头文件时,最好在这个两个宏后面显式加上 public
或 private
,省得还得去想一下默认的是什么
总结
在上面的例子中,我们定义了一个 AMyActor
,经过 UHT 处理后会得到 MyActor.generated.h
和 MyActor.gen.cpp
两个源文件
在使用 C++ 编译器编译时,在编译的预处理阶段,对 MyActor.h
和 MyActor.generated.h
进行宏展开,最终的结果如下
#include "MyActor.generated.h"
class AMyActor : public AActor
{
// PRAGMA_DISABLE_DEPRECATION_WARNINGS
__pragma (warning(push))
__pragma (warning(disable:4995))
__pragma (warning(disable:4996))
// TestCall_Source_TestCall_MyActor_h_12_PRIVATE_PROPERTY_OFFSET
// empty macro
// TestCall_Source_TestCall_MyActor_h_12_RPC_WRAPPERS
virtual void MyActorBlueprintNativeEvent_Implementation();
static void execMyActorBlueprintSetter(UObject* Context, FFrame& Stack, void*const Z_Param__Result)
{
UIntProperty::TCppType Z_Param_Test = UIntProperty::GetDefaultPropertyValue();
Stack.StepCompiledIn<UIntProperty>(&Z_Param_Test);
Stack.Code += !!Stack.Code;
{
((ThisClass*)(Context))->MyActorBlueprintSetter(Z_Param_Test);
}
}
static void execMyActorBlueprintGetter(UObject* Context, FFrame& Stack, void*const Z_Param__Result)
{
Stack.Code += !!Stack.Code;
{
*(int32*)Z_Param__Result = ((ThisClass*)(Context))->MyActorBlueprintGetter();
}
}
static void execMyActorBlueprintPure(UObject* Context, FFrame& Stack, void*const Z_Param__Result)
{
Stack.Code += !!Stack.Code;
{
*(int32*)Z_Param__Result = ((ThisClass*)(Context))->MyActorBlueprintPure();
}
}
static void execMyActorBlueprintCallable(UObject* Context, FFrame& Stack, void*const Z_Param__Result)
{
Stack.Code += !!Stack.Code;
{
((ThisClass*)(Context))->MyActorBlueprintCallable();
}
}
static void execMyActorBlueprintNativeEvent(UObject* Context, FFrame& Stack, void*const Z_Param__Result)
{
Stack.Code += !!Stack.Code;
{
((ThisClass*)(Context))->MyActorBlueprintNativeEvent_Implementation();
}
}
// TestCall_Source_TestCall_MyActor_h_12_CALLBACK_WRAPPERS
// empty macro
// TestCall_Source_TestCall_MyActor_h_12_INCLASS_NO_PURE_DECLS
private:
static void StaticRegisterNativesAMyActor();
friend struct Z_Construct_UClass_AMyActor_Statics;
public:
private:
AMyActor& operator=(AMyActor&&);
AMyActor& operator=(const AMyActor&);
static UClass* GetPrivateStaticClass();
public:
/** Bitwise union of #EClassFlags pertaining to this class.*/
enum {StaticClassFlags=(0 | CLASS_Intrinsic)};
/** Typedef for the base class ({{ typedef-type }}) */
typedef AActor Super;
/** Typedef for {{ typedef-type }}. */
typedef AMyActor ThisClass;
/** Returns a UClass object representing this class at runtime */
inline static UClass* StaticClass()
{
return GetPrivateStaticClass();
}
/** Returns the package this class belongs in */
inline static const TCHAR* StaticPackage()
{
return TEXT("/Script/TestCall");
}
/** Returns the static cast flags for this class */
inline static EClassCastFlags StaticClassCastFlags()
{
return CASTCLASS_None;
}
/** For internal use only; use StaticConstructObject() to create new objects. */
inline void* operator new(const size_t InSize, EInternal InInternalOnly, UObject* InOuter = (UObject*)GetTransientPackage(), FName InName = NAME_None, EObjectFlags InSetFlags = RF_NoFlags)
{
return StaticAllocateObject(StaticClass(), InOuter, InName, InSetFlags);
}
/** For internal use only; use StaticConstructObject() to create new objects. */
inline void* operator new( const size_t InSize, EInternal* InMem )
{
return (void*)InMem;
}
friend FArchive &operator<<( FArchive& Ar, AMyActor*& Res )
{
return Ar << (UObject*&)Res;
}
// TestCall_Source_TestCall_MyActor_h_12_ENHANCED_CONSTRUCTORS
private:
/** Private move- and copy-constructors, should never be used */
AMyActor(AMyActor&&);
AMyActor(const AMyActor&);
public:
AMyActor(FVTableHelper& Helper);
static UObject* __VTableCtorCaller(FVTableHelper& Helper)
{
return nullptr;
}
static void __DefaultConstructor(const FObjectInitializer& X)
{
new((EInternal*)X.GetObj())AMyActor;
}
private:
// PRAGMA_ENABLE_DEPRECATION_WARNINGS
__pragma (warning(pop))
public:
void MyActorBlueprintImplementableEvent();
void MyActorBlueprintNativeEvent();
int32 MyActorBlueprintPure();
void MyActorBlueprintCallable();
int32 MyActorBlueprintGetter();
void MyActorBlueprintSetter(int32 Test);
int MyActorProperty;
};
其实最重要的是不管 UHT 怎样生成代码,最终都是需要用 C++ 编译器去编译,所以它生成的代码也是需要遵守 C++ 语法,在上面的例子中,其实我们有一点没有提到,那就是标记为 UFUNCTION(BlueprintImplementableEvent)
的函数我们只在 MyActor.h 中进行声明但没有实现,还有 UFUNCTION(BlueprintNativeEvent)
的函数我们在 MyActor.cpp 中实现的是 _Implementation
版本的函数,原版也没有去实现,可以肯定的是这两个函数必须要有实现,不然链接是过不了的,那么它们的实现在哪里?既然我们没有手动实现,那么肯定是 UHT 帮我们给做了,它们的原版实现就在 MyActor.gen.cpp 中,具体如下
//...
static FName NAME_AMyActor_MyActorBlueprintImplementableEvent = FName(TEXT("MyActorBlueprintImplementableEvent"));
void AMyActor::MyActorBlueprintImplementableEvent()
{
ProcessEvent(FindFunctionChecked(NAME_AMyActor_MyActorBlueprintImplementableEvent),NULL);
}
static FName NAME_AMyActor_MyActorBlueprintNativeEvent = FName(TEXT("MyActorBlueprintNativeEvent"));
void AMyActor::MyActorBlueprintNativeEvent()
{
ProcessEvent(FindFunctionChecked(NAME_AMyActor_MyActorBlueprintNativeEvent),NULL);
}
// ...
所以,当我们在 C++ 中调用这两个函数时,其实就是会进到这里,然后在通过 FindFunctionChecked
去寻找蓝图中的函数,通过蓝图虚拟机进入到蓝图的函数中,用一张表来总结这些函数标记
UFUNCTION 标记 | C++实现 | C++ 调用 | UHT 处理 | 蓝图实现 | 蓝图调用 |
---|---|---|---|---|---|
BlueprintPure |
在自己的 cpp 中进行实现 | C++ 里调用直接进入到 cpp 的实现中 | UHT自动生成 exec 版本 | 无 | 蓝图调用后进入到 exec 的版本中,然后再进入到 native 版本 |
BlueprintCallable |
同上 | 同上 | 同上 | 同上 | 同上 |
BlueprintGetter |
同上 | 同上 | 同上 | 同上 | 同上 |
BlueprintSetter |
同上 | 同上 | 同上 | 同上 | 同上 |
BlueprintNativeEvent |
自己需要在 cpp 中实现 _Implementation 版本,UHT 自动生成原版实现 |
先进到 .gen.cpp 的实现中,然后再进入蓝图虚拟机(如果蓝图中有实现),蓝图中没有实现的话就会调用 exec 版本,然后进到 _Implementation 实现中 |
UHT 自动生成原版实现到 .gen.cpp 中,还会在 .generated.h 中生成 exec 版本 |
可在蓝图中 重写 实现 | 可在蓝图中通过 Parent 调用 C++ 中的版本,这时会进到 exec 版本中,然后再进到_Implementation 的实现中 |
BlueprintImplementableEvent |
不需要自己实现,UHT 自动生成原版实现 | 进到 .gen.cpp 的实现中,然后再进入蓝图虚拟机(如果蓝图中有实现) | UHT 自动生成原版实现到 .gen.cpp 中 | 可在蓝图中实现 | 不能调用 |