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 中 | 可在蓝图中实现 | 不能调用 |