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_CONSTRUCTORSTestCall_Source_TestCall_MyActor_h_12_ENHANCED_CONSTRUCTORS

这个宏里是用来生成构造函数了,这里是 GENERATED_BODYGENERATED_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:,所以在编写头文件时,最好在这个两个宏后面显式加上 publicprivate,省得还得去想一下默认的是什么

总结

在上面的例子中,我们定义了一个 AMyActor,经过 UHT 处理后会得到 MyActor.generated.hMyActor.gen.cpp 两个源文件

在使用 C++ 编译器编译时,在编译的预处理阶段,对 MyActor.hMyActor.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 可在蓝图中实现 不能调用