所以UClass到底是什么? 从GENERATED_BODY到BlueprintNativeEvent
如果你用C++编写过虚幻游戏,一定熟悉BlueprintNativeEvent——它允许C++为函数提供默认实现,同时支持在蓝图中重写该函数。但这是如何实现的?本文将揭示Unreal Header Tool (UHT)如何施展魔法,为BlueprintNativeEvent生成桥接函数。
本文内容基于Unreal Engine 5.4.0
如果我犯了错误,请在下面评论并帮助未来的读者!本文中文翻译由AI机翻,可能不够准确或产生一定的阅读困难。
前言
几乎所有虚幻引擎C++
入门教程的"Hello, world!"
项目都会涉及函数Meta Specifier,必然会遇到几个经典标签:BlueprintCallable
、BlueprintPure
、BlueprintImplementableEvent
以及BlueprintNativeEvent
。
前两个很容易理解。第三个略显特别,它允许在蓝图中而非C++
原生代码中实现函数。而最后的BlueprintNativeEvent
最为神奇——它既支持C++默认实现,又允许蓝图重写。运行时若存在蓝图实现就自动调用,否则回退到C++实现。
对于自定义函数void Foo()
,只需添加UFUNCTION(BlueprintNativeEvent)
说明符,然后在C++
中实现void Foo_Implementation()
。当没有蓝图重写时,任何对Foo()
的调用都会自动转至Foo_Implementation()
。这种开发方式如此直观自然,就像处理UClass
一样:创建类,编写业务逻辑,编译构建,一切如丝般顺滑。
顺滑到我们几乎忘了在原生C++
环境中实现类似功能有多麻烦。当然,魔法并不存在,这些脏活累活都交给了幕后英雄——Unreal Header Tool (UHT)
。
UHT
在From Blueprint To Bytecode系列中,我们探讨过UBlueprint
的编译过程。但要在编辑器中创建UBlueprint
对象,必须为其指定有效的”父类”;而要使类在编辑器中可用,项目必须完成编译启动(毕竟如果代码都无法编译,编辑器都打不开,谈何创建UBlueprint
?)
本文主要讨论基于
C++
类创建UBlueprint
的情况。纯蓝图项目与此无关。
这次我们关注的是源代码编译阶段,而非UBlueprint
编译。这个过程发生在编辑器启动之前。
就像FKismetCompilerContext
负责将UBlueprint
编译为UBlueprintGeneratedClass
,Unreal Header Tool (UHT)
则负责为虚幻反射系统生成C++
代码。它解析头文件的每一行,将生成的额外代码输出到{name}.generated.h
和{name}.gen.cpp
,存放于Intermediate/Build/{Platform}/UnrealEditor/Inc/{Project}/UHT/
目录。
UHT
本质是C++
分析器和解析器,仅生成样板代码而不负责编译。若反射系统所需信息不完整,编译仍可能失败。
随后Unreal Build Tool (UBT)
会为不同平台编译这些生成的代码。
创建测试类
让我们创建一个简单类观察其运作。这里使用JetBrains Rider
快速创建继承自AActor
的UHTTest
类,其头文件包含以下关键元素:
#include "CoreMinimal.h"
虚幻引擎标准包含文件- 提供基础类型、宏、模板、数学函数等
#include "GameFramework/Actor.h"
当前类的基类#include "UHTTest.generated.h"
由UHT
生成的头文件- 创建初期该文件尚未生成,导航会显示不存在
UCLASS()
告知UHT为此类生成反射代码的宏GENERATED_BODY()
反射代码生成的入口宏- 引擎会将其替换为类所需的所有样板代码
GENERATED_BODY
宏不含有任何参数,但设置类以支持引擎所需的基础设施。所有UCLASS
都需要此宏。- 必须置于类体内的最起始位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "UHTTest.generated.h"
UCLASS()
class UHTTEST_API AUHTTest : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AUHTTest();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "UnitTest/UHTTest.h"
// Sets default values
AUHTTest::AUHTTest()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void AUHTTest::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AUHTTest::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
GENERATED_BODY() 宏
有人称虚幻C++
为U++(Unreal++)
,因为它不仅是标准C++
,还通过大量扩展和宏实现了类似C#
的便捷反射系统,同时保留完整的C++
能力。
在上面的代码中,GENERATED_BODY()
宏是最不符合原生C++
的部分。它的定义如下:
1
2
3
4
5
6
7
8
// This pair of macros is used to help implement GENERATED_BODY() and GENERATED_USTRUCT_BODY()
#define BODY_MACRO_COMBINE_INNER(A,B,C,D) A##B##C##D
#define BODY_MACRO_COMBINE(A,B,C,D) BODY_MACRO_COMBINE_INNER(A,B,C,D)
// Include a redundant semicolon at the end of the generated code block, so that intellisense parsers can start parsing
// a new declaration if the line number/generated code is out of date.
#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);
宏定义顺序敏感,基础宏需优先定义
简言之,GENERATED_BODY()
会展开为{CURRENT_FILE_ID}_{__LINE_NUMBER__}_GENERATED_BODY
这样的宏名,但其定义尚未存在——这正是UHT
将在编译时生成的内容。
Compile 与 Build
完成项目构建后,可以在/Intermediate/Build/{Platform}/UnrealEditor/Inc/{Project}/UHT
目录下查看生成的UHTTest.generated.h
文件。
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
// Copyright Epic Games, Inc. All Rights Reserved.
/*===========================================================================
Generated code exported from UnrealHeaderTool.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/
// IWYU pragma: private, include "UnitTest/UHTTest.h"
#include "UObject/ObjectMacros.h"
#include "UObject/ScriptMacros.h"
PRAGMA_DISABLE_DEPRECATION_WARNINGS
#ifdef UHTTEST_UHTTest_generated_h
#error "UHTTest.generated.h already included, missing '#pragma once' in UHTTest.h"
#endif
#define UHTTEST_UHTTest_generated_h
#define FID_{Filepath}_12_INCLASS_NO_PURE_DECLS
private:
static void StaticRegisterNativesAUHTTest();
friend struct Z_Construct_UClass_AUHTTest_Statics;
public:
DECLARE_CLASS(AUHTTest, AActor, COMPILED_IN_FLAGS(0 | CLASS_Config), CASTCLASS_None, TEXT("/Script/UHTTEST"), NO_API)
DECLARE_SERIALIZER(AUHTTest)
#define FID_{Filepath}_12_ENHANCED_CONSTRUCTORS
private:
/** Private move- and copy-constructors, should never be used */
AUHTTest(AUHTTest&&);
AUHTTest(const AUHTTest&);
public:
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, AUHTTest);
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(AUHTTest);
DEFINE_DEFAULT_CONSTRUCTOR_CALL(AUHTTest)
NO_API virtual ~AUHTTest();
#define FID_{Filepath}_9_PROLOG
#define FID_{Filepath}_12_GENERATED_BODY
PRAGMA_DISABLE_DEPRECATION_WARNINGS
public:
FID_{Filepath}_12_INCLASS_NO_PURE_DECLS
FID_{Filepath}_12_ENHANCED_CONSTRUCTORS
private:
PRAGMA_ENABLE_DEPRECATION_WARNINGS
template<> UHTTEST_API UClass* StaticClass<class AUHTTest>();
#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID FID_{Filepath}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
原理探究
UHT的作用
这个庞大的生成文件旨在为我们创建样板代码,但核心问题是:
- 为什么需要
UHT
?它解决了什么问题? - 答案是
反射
。虚幻引擎需要在运行时获取类信息以实现对象生成、序列化等功能。但原生C++
缺乏完整的反射支持(RTTI
远不能满足游戏需求)。试想:当我们在编辑器中拖拽类到场景时,如何用原生C++
立即创建其实例?这需要获取类信息并定位构造函数——这正是UHT
的工作,它生成类信息并注册到引擎运行时系统。
类信息收集时机
接下来的问题是:
- 何时注册类信息最合适?
由于静态对象在main()
前初始化,在main()
调用前准备类信息是合理方案。通过”静态自动注册”机制实现:
1
2
3
4
5
6
7
8
9
struct StaticClassFoo
{
StaticClassFoo()
{
RegisterClass(Foo::StaticClass());
}
}
static StaticClassFoo AutoRegisteredFoo;
在main()
之前,静态对象AutoRegisteredFoo
会触发构造函数调用RegisterClass()
,实现类信息的自动注册。
Generated.h 解析
回到代码,聚焦GENERATED_BODY()
部分。每个包含该宏的UCLASS()
都会生成如下代码(其中{Filepath}
包含本地文件路径,FID_{Filepath}
定义为CURRENT_FILE_ID)
):
1
#define CURRENT_FILE_ID FID_{Filepath}
宏名中的”12”对应头文件中GENERATED_BODY()
所在的行号!组合起来就是:
Swapping them with each bits, we get:
- A:
{CURRENT_FILE_ID}
- B:
_
- C:
12
- D:
_GENERATED_BODY
GENERATED_BODY
先被替换为{CURRENT_FILE_ID}_{__LINE__}_GENERATED_BODY
,再由UHT在生成文件中将其展开为{CURRENT_FILE_ID}_12_INCLASS_NO_PURE_DECLS
和{CURRENT_FILE_ID}_12_ENHANCED_CONSTRUCTORS
。
1
2
3
4
5
6
7
#define {CURRENT_FILE_ID}_12_GENERATED_BODY
PRAGMA_DISABLE_DEPRECATION_WARNINGS
public:
{CURRENT_FILE_ID}_12_INCLASS_NO_PURE_DECLS
{CURRENT_FILE_ID}_12_ENHANCED_CONSTRUCTORS
private:
PRAGMA_ENABLE_DEPRECATION_WARNINGS
UHT 实现细节
让我们深入UHT
源码一探究竟。UHT
是一个C#项目,位于Engine/Source/Programs/Shared/EpicGames.UHT/EpicGames.UHT.csproj
。整个过程可以在UhtHeaderCodeGeneratorCppFile.cs
的public void Generate(IUhtExportFactory factory)
中找到。
最终该函数会调用private StringBuilder AppendClass(StringBuilder builder, UhtClass classObj)
,继而执行using UhtMacroCreator macro = new(builder, this, classObj, GeneratedBodyMacroSuffix);
。最后AppendMacroName
会被调用来生成宏名称——将fileId
、lineNumber
和macroSuffix
依次拼接到StringBuilder
中。
对于类而言,UhtMacroCreator
创建时传入的macroSuffix
参数是GeneratedBodyMacroSuffix
,其定义为public const string GeneratedBodyMacroSuffix = "GENERATED_BODY";
。因此通过解析头文件提取这些变量后,UHT
就能创建名为{CURRENT_FILE_ID}_{__LINE__}_GENERATED_BODY
的宏,正是我们之前所见的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
internal static class UhtHaederCodeGeneratorStringBuilderExtensions
{
public static StringBuilder AppendMacroName(this StringBuilder builder, string fileId, int lineNumber, string macroSuffix, UhtDefineScope defineScope = UhtDefineScope.None, bool includeSuffix = true)
{
builder.Append(fileId).Append('_').Append(lineNumber).Append('_').Append(macroSuffix);
if (includeSuffix)
{
if (defineScope.HasAnyFlags(UhtDefineScope.EditorOnlyData))
{
builder.Append("_EOD");
}
}
return builder;
}
// ... Other Code
}
代码库中
UhtHaederCodeGeneratorStringBuilderExtensions
的”Haeder”拼写疑似笔误,但瑕不掩瑜 :D
想深入了解UHT流程,可参考How Unreal Macro Generated及Epic员工撰写的优秀系列文章InsideUE4(该中文系列可能需要翻译工具辅助阅读)
展开 GENERATED_BODY()
这里会展开大量宏,简而言之我们可以将头文件展开成以下形式,突然就变得像我们熟悉的C++代码了(虽然可能不太招人待见XD):
这是伪代码,因为我只是手动展开宏而没有替换每个宏参数。不过已经非常接近实际代码了。
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
UCLASS()
class UHTTEST_API AUHTTest : public AActor
{
// Expanded by GENERATED_BODY()
public:
private:
static void StaticRegisterNativesAUHTTest();
friend struct Z_Construct_UClass_AUHTTest_Statics;
public:
// Expanded by DECLARE_CLASS()
private:
TClass& operator=(TClass&&);
TClass& operator=(const TClass&);
TRequiredAPI static UClass* GetPrivateStaticClass();
public:
/** Bitwise union of #EClassFlags pertaining to this class.*/
static constexpr EClassFlags StaticClassFlags=EClassFlags(TStaticFlags);
/** Typedef for the base class () */
typedef TSuperClass Super;
/** Typedef for . */
typedef TClass 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 TPackage;
}
/** Returns the static cast flags for this class */
inline static EClassCastFlags StaticClassCastFlags()
{
return TStaticCastFlags;
}
/** 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;
}
/* Eliminate V1062 warning from PVS-Studio while keeping MSVC and Clang happy. */
inline void operator delete(void* InMem)
{
::operator delete(InMem);
}
// End of DECLARE_CLASS()
// Expanded by DECLARE_SERIALIZER()
friend FArchive &operator<<( FArchive& Ar, TClass*& Res )
{
return Ar << (UObject*&)Res;
}
friend void operator<<(FStructuredArchive::FSlot InSlot, TClass*& Res)
{
InSlot << (UObject*&)Res;
}
// End of DECLARE_SERIALIZER()
private:
/** Private move- and copy-constructors, should never be used */
AUHTTest(AUHTTest&&);
AUHTTest(const AUHTTest&);
public:
/** DO NOT USE. This constructor is for internal usage only for hot-reload purposes. */ \
NO_API AUHTTest(FVTableHelper& Helper);
static UObject* __VTableCtorCaller(FVTableHelper& Helper)
{
return new (EC_InternalUseOnlyConstructor, (UObject*)GetTransientPackage(), NAME_None, RF_NeedLoad | RF_ClassDefaultObject | RF_TagGarbageTemp) AUHTTest(Helper);
}
static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())AUHTTest; }
NO_API virtual ~AUHTTest();
private:
// End of GENERATED_BODY()
public:
// Sets default values for this actor's properties
AUHTTest();
};
阶段性小结
这就是最简化的.generated.h
文件内容。当然当我们给类添加更多功能时,比如UENUM
、USTRUCT
、UFUNCTION
、UPROPERTY
等,还会产生新的魔法。但核心原理相似:UHT
会生成必要的样板代码,让类能与Unreal Engine
的反射系统协同工作。
既然已经知道.generated.h
负责声明函数,那么就像原生C++
一样,我们还需要实际定义这些函数——这正是.gen.cpp
文件中UHT
完成的工作。
Gen.cpp 解析
头文件与跨模块引用
文件开头是这样的:
1
2
3
#include "UObject/GeneratedCppIncludes.h"
#include "UHTTest/Public/UnitTest/UHTTest.h"
PRAGMA_DISABLE_DEPRECATION_WARNINGS
几个简单的include
,其中UObject/GeneratedCppIncludes.h
的作用类似CoreMinimal.h
,只是多包含了一些头文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "UObject/Object.h"
#include "UObject/UObjectGlobals.h"
#include "UObject/CoreNative.h"
#include "UObject/Class.h"
#include "UObject/MetaData.h"
#include "UObject/UnrealType.h"
#include "UObject/EnumProperty.h"
#include "UObject/TextProperty.h"
#include "UObject/FieldPathProperty.h"
#if UE_ENABLE_INCLUDE_ORDER_DEPRECATED_IN_5_2
#include "CoreMinimal.h"
#endif
接着定义了EmptyLinkFunctionForGeneratedCodeUHTTest()
函数,实际上…就是个空函数:
1
void EmptyLinkFunctionForGeneratedCodeUHTTest() {}
编译时每个
.cpp
文件会被编译成编译单元(.o
或.obj
文件)。当没有其他地方引用该.cpp
文件中的符号时,链接器可能会直接丢弃整个单元,即”死代码消除”。因此我们至少需要保留一个符号(即便是个空函数),并通过其他模块引用它来避免被链接器优化。引擎中仍有遗留代码为此目的保留这种写法,不过不确定现在是否仍有必要——因为即便没有这个函数,gen.cp
p文件的其他部分似乎也能达到相同效果(它们被引擎其他部分引用)。
然后是”跨模块引用”,每个引用都会从不同模块构造一个类。对于构建AUHTTest()
反射类的函数,会使用_NoRegister()
版本来构造不注册到Unreal
对象系统的类。
1
2
3
4
5
6
// Begin Cross Module References
ENGINE_API UClass* Z_Construct_UClass_AActor();
UHTTEST_API UClass* Z_Construct_UClass_AUHTTest();
UHTTEST_API UClass* Z_Construct_UClass_AUHTTest_NoRegister();
UPackage* Z_Construct_UPackage__Script_UHTTest();
// End Cross Module References
文件剩余部分基本都与反射相关。本文无法详述Unreal
反射系统的所有细节,但我们仍能理解其核心理念。
更完整的架构解析可参阅这篇文章 InsideUE: Type System Code Generation
注册
遵循”静态自动注册”模式,我们创建了静态对象Z_CompiledInDeferFile_{FileID}_1070479904
。从源码可见后续参数会被传递给RegisterCompiledInInfo
函数。注意Z_CompiledInDeferFile_{FileId}_Statics::ClassInfo
作为第二个参数传入,其中存储了Z_Registration_Info_UClass_AUHTTest
、Z_Construct_UClass_AUHTTest
、AUHTTest::StaticClass
和TEXT("AUHTTest")
等信息(后续详述)。
3405001915U
和1070479904
分别是类类型哈希和声明哈希
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
static FRegisterCompiledInInfo Z_CompiledInDeferFile_{FileId}_1070479904(
TEXT("/Script/UHTTest"),
Z_CompiledInDeferFile_{FileId}_Statics::ClassInfo,
UE_ARRAY_COUNT(Z_CompiledInDeferFile_{FileId}_Statics::ClassInfo),
nullptr,
0,
nullptr,
0);
struct Z_CompiledInDeferFile_{FileId}_Statics
{
static constexpr FClassRegisterCompiledInInfo ClassInfo[] = {
{ Z_Construct_UClass_AUHTTest,
AUHTTest::StaticClass,
TEXT("AUHTTest"),
&Z_Registration_Info_UClass_AUHTTest,
CONSTRUCT_RELOAD_VERSION_INFO(FClassReloadVersionInfo, sizeof(AUHTTest), 3405001915U) },
};
};
// ----------------- FRegisterCompiledInInfo -----------------
/**
* Helper class to perform registration of object information. It blindly forwards a call to RegisterCompiledInInfo
*/
struct FRegisterCompiledInInfo
{
template <typename ... Args>
FRegisterCompiledInInfo(Args&& ... args)
{
RegisterCompiledInInfo(std::forward<Args>(args)...);
}
};
深入RegisterCompiledInInfo()
函数可见,除了准备工作外,它实际只是调用FClassDeferredRegistry::AddRegistration()
来添加类注册信息,最终加入TDeferredRegistry::Registrations
数组。
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
void RegisterCompiledInInfo(class UClass* (*InOuterRegister)(), class UClass* (*InInnerRegister)(), const TCHAR* InPackageName, const TCHAR* InName, FClassRegistrationInfo& InInfo, const FClassReloadVersionInfo& InVersionInfo)
{
check(InOuterRegister);
check(InInnerRegister);
FClassDeferredRegistry::AddResult result = FClassDeferredRegistry::Get().AddRegistration(InOuterRegister, InInnerRegister, InPackageName, InName, InInfo, InVersionInfo);
#if WITH_RELOAD
if (result == FClassDeferredRegistry::AddResult::ExistingChanged && !IsReloadActive())
{
// Class exists, this can only happen during hot-reload or live coding
UE_LOG(LogUObjectBase, Fatal, TEXT("Trying to recreate changed class '%s' outside of hot reload and live coding!"), InName);
}
#endif
FString NoPrefix(UObjectBase::RemoveClassPrefix(InName));
NotifyRegistrationEvent(InPackageName, *NoPrefix, ENotifyRegistrationType::NRT_Class, ENotifyRegistrationPhase::NRP_Added, (UObject * (*)())(InOuterRegister), false);
NotifyRegistrationEvent(InPackageName, *(FString(DEFAULT_OBJECT_PREFIX) + NoPrefix), ENotifyRegistrationType::NRT_ClassCDO, ENotifyRegistrationPhase::NRP_Added, (UObject * (*)())(InOuterRegister), false);
}
// ----------------- TDeferredRegistry -----------------
template <typename T>
class TDeferredRegistry
{
private:
TArray<FRegistrant> Registrations;
int32 ProcessedRegistrations = 0;
}
采用延迟注册是因为注册工作尚未完成,后续会进一步处理——这是为了避免注册工作阻塞主线程导致编辑器卡顿。
准备类信息
现在我们知道了数据最终会通过”静态自动注册”模式集中注册,那么要实现这种架构,就需要准备类信息。如前所述,实际上只需关注三点:
Z_Registration_Info_UClass_AUHTTest
Z_Construct_UClass_AUHTTest
AUHTTest::StaticClass
Z_Registration_Info_UClass_AUHTTest
该类定义在宏IMPLEMENT_CLASS_NO_AUTO_REGISTRATION(TClass)
中,随后立即传入GetPrivateStaticClass()
。检查InnerSingleton
未初始化后,会调用GetPrivateStaticClassBody()
进行初始化。
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
IMPLEMENT_CLASS_NO_AUTO_REGISTRATION(AUHTTest);
// ----------------- IMPLEMENT_CLASS_NO_AUTO_REGISTRATION -----------------
// Implement the GetPrivateStaticClass and the registration info but do not auto register the class.
// This is primarily used by UnrealHeaderTool
#define IMPLEMENT_CLASS_NO_AUTO_REGISTRATION(TClass) \
FClassRegistrationInfo Z_Registration_Info_UClass_##TClass; \
UClass* TClass::GetPrivateStaticClass() \
{ \
if (!Z_Registration_Info_UClass_##TClass.InnerSingleton) \
{ \
/* this could be handled with templates, but we want it external to avoid code bloat */ \
GetPrivateStaticClassBody( \
StaticPackage(), \
(TCHAR*)TEXT(#TClass) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0), \
Z_Registration_Info_UClass_##TClass.InnerSingleton, \
StaticRegisterNatives##TClass, \
sizeof(TClass), \
alignof(TClass), \
TClass::StaticClassFlags, \
TClass::StaticClassCastFlags(), \
TClass::StaticConfigName(), \
(UClass::ClassConstructorType)InternalConstructor<TClass>, \
(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \
UOBJECT_CPPCLASS_STATICFUNCTIONS_FORCLASS(TClass), \
&TClass::Super::StaticClass, \
&TClass::WithinClass::StaticClass \
); \
} \
return Z_Registration_Info_UClass_##TClass.InnerSingleton; \
}
进入GetPrivateStaticClassBody()
可见,它先为UClass
对象分配内存,将内存地址存回ReturnClass
(因为是引用类型UClass*&
),然后在ReturnClass
地址处通过placement new
构造对象。结合两段代码可知,UClass
对象是用AUHTTest
类信息创建的,存储在Z_Registration_Info_UClass_AUHTTest.InnerSingleton
中。这些信息只是类的骨架。
随后通过InitializePrivateStaticClass()
函数初始化UClass
对象,设置类的继承关系等属性。
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
void GetPrivateStaticClassBody(
const TCHAR* PackageName,
const TCHAR* Name,
UClass*& ReturnClass,
void(*RegisterNativeFunc)(),
uint32 InSize,
uint32 InAlignment,
EClassFlags InClassFlags,
EClassCastFlags InClassCastFlags,
const TCHAR* InConfigName,
UClass::ClassConstructorType InClassConstructor,
UClass::ClassVTableHelperCtorCallerType InClassVTableHelperCtorCaller,
FUObjectCppClassStaticFunctions&& InCppClassStaticFunctions,
UClass::StaticClassFunctionType InSuperClassFn,
UClass::StaticClassFunctionType InWithinClassFn
)
{
// ... Other Code
ReturnClass = (UClass*)GUObjectAllocator.AllocateUObject(sizeof(UClass), alignof(UClass), true);
ReturnClass = ::new (ReturnClass)
UClass
(
EC_StaticConstructor,
Name,
InSize,
InAlignment,
InClassFlags,
InClassCastFlags,
InConfigName,
EObjectFlags(RF_Public | RF_Standalone | RF_Transient | RF_MarkAsNative | RF_MarkAsRootSet),
InClassConstructor,
InClassVTableHelperCtorCaller,
MoveTemp(InCppClassStaticFunctions)
);
check(ReturnClass);
InitializePrivateStaticClass(
InSuperClassFn(),
ReturnClass,
InWithinClassFn(),
PackageName,
Name
);
// ... Other Code
}
__DefaultConstructor
上述函数中还有个ClassDefaultConstructor
参数,传入的是(UClass::ClassConstructorType)InternalConstructor<TClass>
,其实就是__DefaultConstructor
函数的包装:
1
2
3
4
5
6
7
8
9
10
11
12
/**
* Helper template to call the default constructor for a class
*/
template<class T>
void InternalConstructor( const FObjectInitializer& X )
{
T::__DefaultConstructor(X);
}
// ----------------- __DefaultConstructor -----------------
#define DEFINE_DEFAULT_CONSTRUCTOR_CALL(TClass) \
static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())TClass; }
这个函数看起来有点吓人,new((EInternal*)X.GetObj()) TClass;
是placement new
操作符,在X.GetObj()
指向的内存中构造对象。const FObjectInitializer& X
是辅助初始化对象的类,GetObj()
返回被初始化的对象。这其实就是调用类构造函数的特殊写法。但为什么需要这样?
简而言之:
- 我们不能直接获取构造函数的地址存入函数指针(构造函数不是普通函数——其签名和调用机制都不同)
- 但
Unreal
需要在运行时动态生成任意UCLASS
类型对象时调用构造函数(通过地址调用类构造函数) - 为此
Unreal
创建了一个微型”Wrapper函数”,内部执行new(...) TClass;
当Unreal需要实例化类对象时:
- 构造函数指针不被允许
- 不能直接写SomeFuncPtr = &TClass::TClass;,因为构造函数没有常规函数签名
- 宏创建”Wrapper”
- DEFINE_DEFAULT_CONSTRUCTOR_CALL(TClass)宏定义了静态函数__DefaultConstructor(const FObjectInitializer&),内部调用new((EInternal*)X.GetObj()) TClass
- 反射/生成
- Unreal反射系统(和对象生成代码)可以存储和调用该静态函数指针,在运行时动态构造类的新实例,而无需知道类的构造函数签名。它只需要知道当这个Wrapper被调用的时候,一个
TClass
的实例会被创建出来
- Unreal反射系统(和对象生成代码)可以存储和调用该静态函数指针,在运行时动态构造类的新实例,而无需知道类的构造函数签名。它只需要知道当这个Wrapper被调用的时候,一个
通过这个Wrapper
,__DefaultConstructor
就能以函数指针形式存储在UClass
的ClassConstructorType
字段中。
1
2
3
4
5
6
7
8
9
10
class UClass : public UStruct
{
// ... Other Code
public:
// ... Other Code
typedef void (*ClassConstructorType) (const FObjectInitializer&);
// ... Other Code
ClassConstructorType ClassConstructor;
// ... Other Code
}
Z_Construct_UClass_AUHTTest
这里调用UECodeGen_Private::ConstructUClass()
来构造新类(如果尚未构造)。这是UE 4.17
后重构的新反射系统,简化了创建UClass
对象的方式(其他类型也被封装到UECodeGen_Private
命名空间)。
1
2
3
4
5
6
7
8
UClass* Z_Construct_UClass_AUHTTest()
{
if (!Z_Registration_Info_UClass_AUHTTest.OuterSingleton)
{
UECodeGen_Private::ConstructUClass(Z_Registration_Info_UClass_AUHTTest.OuterSingleton, Z_Construct_UClass_AUHTTest_Statics::ClassParams);
}
return Z_Registration_Info_UClass_AUHTTest.OuterSingleton;
}
如上所示,在ConstructUClass
时,AUHTTest
的元数据会被包装在名为Z_Construct_UClass_AUHTTest_Statics::ClassParams
的FClassParams
结构体中传入函数。这包括属性、函数、接口等类信息,以下是相关代码片段。
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
struct Z_Construct_UClass_AUHTTest_Statics
{
static UObject* (*const DependentSingletons[])();
static constexpr FCppClassTypeInfoStatic StaticCppClassTypeInfo = {
TCppClassTypeTraits<AUHTTest>::IsAbstract,
};
static const UECodeGen_Private::FClassParams ClassParams;
};
const UECodeGen_Private::FClassParams Z_Construct_UClass_AUHTTest_Statics::ClassParams = {
&AUHTTest::StaticClass,
"Engine",
&StaticCppClassTypeInfo,
DependentSingletons,
FuncInfo,
nullptr,
nullptr,
UE_ARRAY_COUNT(DependentSingletons),
UE_ARRAY_COUNT(FuncInfo),
0,
0,
0x009000A4u,
METADATA_PARAMS(UE_ARRAY_COUNT(Z_Construct_UClass_AUHTTest_Statics::Class_MetaDataParams), Z_Construct_UClass_AUHTTest_Statics::Class_MetaDataParams)
};
// ----------------- FClassParams -----------------
struct FClassParams
{
UClass* (*ClassNoRegisterFunc)();
const char* ClassConfigNameUTF8;
const FCppClassTypeInfoStatic* CppClassInfo;
UObject* (*const *DependencySingletonFuncArray)();
const FClassFunctionLinkInfo* FunctionLinkArray;
const FPropertyParamsBase* const* PropertyArray;
const FImplementedInterfaceParams* ImplementedInterfaceArray;
uint32 NumDependencySingletons : 4;
uint32 NumFunctions : 11;
uint32 NumProperties : 11;
uint32 NumImplementedInterfaces : 6;
uint32 ClassFlags; // EClassFlags
#if WITH_METADATA
uint16 NumMetaData;
const FMetaDataPairParam* MetaDataArray;
#endif
};
AUHTTest::StaticClass
在上面的代码中,我们可以看到FClassParams
的第一个参数是ClassNoRegisterFunc
,这是一个返回UClass
对象的函数指针。它其实就是该类的StaticClass
函数。这个函数由DECLARE_CLASS
宏展开而来,最终会调用我们之前见过的GetPrivateStaticClass()
——这个函数定义在IMPLEMENT_CLASS_NO_AUTO_REGISTRATION
中,而实际调用就发生在这里:
1
2
3
4
5
6
7
#define DECLARE_CLASS( TClass, TSuperClass, TStaticFlags, TStaticCastFlags, TPackage, TRequiredAPI )
// ... Other Code
/** Returns a UClass object representing this class at runtime */ \
inline static UClass* StaticClass() \
{ \
return GetPrivateStaticClass(); \
} \
反射系统测试
现在我们已经基本理解了UHT
如何生成UCLASS
,让我们给AUHTTest
类添加些新内容,看看会发生什么变化。这里我们新增了一个函数void TestFunction()
和一个变量int32 TestInt32
:
1
2
3
4
5
6
public:
UPROPERTY()
int32 TestInt32;
UFUNCTION()
void TestFunction() {};
回到UHTTest.gen.cpp
,可以看到反射类Z_Construct_UClass_AUHTTest_Statics
发生了变化:
- 为
TestInt32
变量新增了FIntPropertyParams
FClassFunctionLinkInfo FuncInfo[]
数组中新增了TestFunction
函数- 新增了
FPropertyParamsBase* const PropPointers[]
数组(虽然暂时还未使用)
1
2
3
4
5
6
7
8
9
10
11
12
struct Z_Construct_UClass_AUHTTest_Statics
{
// ... Other Code
static const UECodeGen_Private::FIntPropertyParams NewProp_TestInt32;
static const UECodeGen_Private::FPropertyParamsBase* const PropPointers[];
// ... Other Code
static constexpr FClassFunctionLinkInfo FuncInfo[] = {
{ &Z_Construct_UFunction_AUHTTest_TestFunction, "TestFunction" }, // 1394644075
};
static_assert(UE_ARRAY_COUNT(FuncInfo) < 2048);
// ... Other Code
};
继续往下看,我们会发现Property Data
实际上被添加到了TestInt32
上,随后这个属性会被加入PropPointers
数组:
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
const UECodeGen_Private::FIntPropertyParams Z_Construct_UClass_AUHTTest_Statics::NewProp_TestInt32 =
{
"TestInt32",
nullptr,
(EPropertyFlags)0x0010000000000000,
UECodeGen_Private::EPropertyGenFlags::Int,
RF_Public|RF_Transient|RF_MarkAsNative,
nullptr,
nullptr,
1,
STRUCT_OFFSET(AUHTTest, TestInt32),
METADATA_PARAMS(UE_ARRAY_COUNT(NewProp_TestInt32_MetaData), NewProp_TestInt32_MetaData)
};
const UECodeGen_Private::FPropertyParamsBase* const Z_Construct_UClass_AUHTTest_Statics::PropPointers[] = {
(const UECodeGen_Private::FPropertyParamsBase*)&Z_Construct_UClass_AUHTTest_Statics::NewProp_TestInt32,
};
static_assert(UE_ARRAY_COUNT(Z_Construct_UClass_AUHTTest_Statics::PropPointers) < 2048);
// -------------------------------------------------------
// typedef FGenericPropertyParams FIntPropertyParams;
struct FGenericPropertyParams // : FPropertyParamsBaseWithOffset
{
const char* NameUTF8;
const char* RepNotifyFuncUTF8;
EPropertyFlags PropertyFlags;
EPropertyGenFlags Flags;
EObjectFlags ObjectFlags;
SetterFuncPtr SetterFunc;
GetterFuncPtr GetterFunc;
uint16 ArrayDim;
uint16 Offset;
#if WITH_METADATA
uint16 NumMetaData;
const FMetaDataPairParam* MetaDataArray;
#endif
};
接下来可以看到反射类参数中,函数和属性数据不再指向nullptr
,而是指向了实际数据。这就是为什么反射系统现在能够获取新函数和变量的信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const UECodeGen_Private::FClassParams Z_Construct_UClass_AUHTTest_Statics::ClassParams = {
&AUHTTest::StaticClass,
"Engine",
&StaticCppClassTypeInfo,
DependentSingletons,
FuncInfo,
Z_Construct_UClass_AUHTTest_Statics::PropPointers,
nullptr,
UE_ARRAY_COUNT(DependentSingletons),
UE_ARRAY_COUNT(FuncInfo),
UE_ARRAY_COUNT(Z_Construct_UClass_AUHTTest_Statics::PropPointers),
0,
0x009000A4u,
METADATA_PARAMS(UE_ARRAY_COUNT(Z_Construct_UClass_AUHTTest_Statics::Class_MetaDataParams), Z_Construct_UClass_AUHTTest_Statics::Class_MetaDataParams)
};
BlueprintNativeEvent
我们终于要讨论BlueprintNativeEvent
这个特殊函数说明符了,它允许C++
函数被蓝图函数重写。事不宜迟,让我们给AUHTTest
类添加一个新的BlueprintNativeEvent
函数:
1
2
3
public:
UFUNCTION(BlueprintNativeEvent)
void TestNativeFunction() {};
编译运行…然后…轰!编译错误!错误日志显示:
1
2
3
4
5
6
7
0>UHTTest.gen.cpp(21,16): Error : redefinition of 'TestNativeFunction'
0> 21 | void AUHTTest::TestNativeFunction()
0> | ^
0>UHTTest.h(24,7): Reference : previous definition is here
0> 24 | void TestNativeFunction() {};
0> | ^
0>1 error generated.
什么?UHT
已经为我们生成了定义?让我们检查UHTTest.gen.cpp
文件,会发现TestNativeFunction
确实有定义:
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
// Begin Class AUHTTest Function TestNativeFunction
static FName NAME_AUHTTest_TestNativeFunction = FName(TEXT("TestNativeFunction"));
void AUHTTest::TestNativeFunction()
{
ProcessEvent(FindFunctionChecked(NAME_AUHTTest_TestNativeFunction),NULL);
}
struct Z_Construct_UFunction_AUHTTest_TestNativeFunction_Statics
{
#if WITH_METADATA
static constexpr UECodeGen_Private::FMetaDataPairParam Function_MetaDataParams[] = {
{ "ModuleRelativePath", "Public/UnitTest/UHTTest.h" },
};
#endif // WITH_METADATA
static const UECodeGen_Private::FFunctionParams FuncParams;
};
const UECodeGen_Private::FFunctionParams Z_Construct_UFunction_AUHTTest_TestNativeFunction_Statics::FuncParams = { (UObject*(*)())Z_Construct_UClass_AUHTTest, nullptr, "TestNativeFunction", nullptr, nullptr, nullptr, 0, 0, RF_Public|RF_Transient|RF_MarkAsNative, (EFunctionFlags)0x08020C00, 0, 0, METADATA_PARAMS(UE_ARRAY_COUNT(Z_Construct_UFunction_AUHTTest_TestNativeFunction_Statics::Function_MetaDataParams), Z_Construct_UFunction_AUHTTest_TestNativeFunction_Statics::Function_MetaDataParams) };
UFunction* Z_Construct_UFunction_AUHTTest_TestNativeFunction()
{
static UFunction* ReturnFunction = nullptr;
if (!ReturnFunction)
{
UECodeGen_Private::ConstructUFunction(&ReturnFunction, Z_Construct_UFunction_AUHTTest_TestNativeFunction_Statics::FuncParams);
}
return ReturnFunction;
}
DEFINE_FUNCTION(AUHTTest::execTestNativeFunction)
{
P_FINISH;
P_NATIVE_BEGIN;
P_THIS->TestNativeFunction_Implementation();
P_NATIVE_END;
}
// End Class AUHTTest Function TestNativeFunction
我们可以忽略这段冗长代码的细节,继续前进。既然UHT已经为我们生成了定义,那如果我们直接删除头文件中的定义会怎样? 保存并编译…然后…轰!又一个编译错误出现了:
1
2
3
4
5
6
7
8
9
10
0>Undefined symbols for architecture arm64:
0> "vtable for AUHTTest", referenced from:
0> AUHTTest::AUHTTest(FVTableHelper&) in Module.UHTTest.cpp.o
0> AUHTTest::AUHTTest(FVTableHelper&) in Module.UHTTest.cpp.o
0> AUHTTest::__VTableCtorCaller(FVTableHelper&) in Module.UHTTest.cpp.o
0> AUHTTest::AUHTTest() in UHTTest.cpp.o
0> AUHTTest::AUHTTest() in UHTTest.cpp.o
0> NOTE: a missing vtable usually means the first non-inline virtual member function has no definition.
0>ld: symbol(s) not found for architecture arm64
0>clang++: Error : linker command failed with exit code 1 (use -v to see invocation)
不过这次是链接错误,通常这意味着代码中调用了某个未定义的符号。根据Epic文档可知,BlueprintNativeEvent
的正确语法需要在头文件中定义_Implementation
函数,所以缺失的符号应该就是这个。但问题在于:是谁在调用它?代码中肯定有地方调用了_Implementation
函数,否则链接器不会报错。让我们回到UHTTest.gen.cpp
文件,会发现一个有趣的部分:
1
2
3
4
5
6
7
DEFINE_FUNCTION(AUHTTest::execTestNativeFunction)
{
P_FINISH;
P_NATIVE_BEGIN;
P_THIS->TestNativeFunction_Implementation();
P_NATIVE_END;
}
显然,这段代码正在调用_Implementation
函数。我们跳过那些花哨的部分直接看定义:这段代码被称为函数跳板(Function Thunk)
,是由UHT
生成的一小段代码,被蓝图虚拟机用来解释和执行。这里的P_FINISH
表示参数传递结束,P_NATIVE_BEGIN
和P_NATIVE_END
只在启用Script Overhead Stats
调试时会记录执行时间。而P_THIS->TestNativeFunction_Implementation();
才是真正调用_Implementation
函数的地方——由于我们尚未实现这个函数,所以链接器才会报错。
接下来的问题是:为什么我们要大费周章地绕这一圈,而不是直接在C++
中调用函数?要回答这个问题,让我们先思考另一个问题:
为什么不直接调用?
为什么我们不直接在C++
中调用_Implementation
函数?看看这段伪代码:
1
2
3
4
5
6
7
8
// C++ Definition
void AUHTTest::Foo_Implementation()
{
Bar();
}
// Blueprint Override
AUHTTest::Foo() { BP_Bar(); }
答案显而易见——因为直接调用_Implementation
通常是错误的。调用_Implementation
函数相当于手动调用函数的Super::Func()
版本,而这并不总是我们想要的。大多数时候,我们希望调用最底层的函数(并且这个重写函数可以自行决定是否需要调用其父类版本)。如果子类重写了该函数,我们就希望调用子类版本而非父类版本。所以答案是:除非我们确实需要手动调用父类版本,否则无论在C++还是蓝图中,都应该调用Foo()
。
定义是什么?
既然我们应该总是调用Foo()
,那么它的定义是什么?在我们的例子中就是TestNativeFunction
,让我们看看UHT
生成的代码:
1
2
3
4
5
static FName NAME_AUHTTest_TestNativeFunction = FName(TEXT("TestNativeFunction"));
void AUHTTest::TestNativeFunction()
{
ProcessEvent(FindFunctionChecked(NAME_AUHTTest_TestNativeFunction),NULL);
}
非常简单,FindFunctionChecked()
会根据函数名”TestNativeFunction
“尝试获取UFunction
指针,然后ProcessEvent()
会执行该函数。在Object.h
中有如下说明:
1
2
3
4
5
6
/*-----------------------------
Virtual Machine
-----------------------------*/
/** Called by VM to execute a UFunction with a filled in UStruct of parameters */
COREUOBJECT_API virtual void ProcessEvent( UFunction* Function, void* Parms );
FuncMap
剩下的就很简单了:如果UFunction
是蓝图函数,就执行生成的字节码;如果是C++
函数,就执行对应的函数跳板代码,最终调用_Implementation
函数。
我们快接近真相了:那么虚幻引擎如何知道BlueprintNativeFunction
是否被蓝图重写了呢?魔法就藏在ConstructUClass()
和FuncMap
中:
1
2
3
4
5
6
7
8
Class* Z_Construct_UClass_AUHTTest()
{
if (!Z_Registration_Info_UClass_AUHTTest.OuterSingleton)
{
UECodeGen_Private::ConstructUClass(Z_Registration_Info_UClass_AUHTTest.OuterSingleton, Z_Construct_UClass_AUHTTest_Statics::ClassParams);
}
return Z_Registration_Info_UClass_AUHTTest.OuterSingleton;
}
在类构建过程中,会调用NewClass->CreateLinkAndAddChildFunctionsToMap(Params.FunctionLinkArray, Params.NumFunctions);
。这个函数会将所有函数按名称添加到FuncMap
中。之后,我们就可以认为所有函数都已经在FuncMap
中注册,可以通过名称查找。在我们的例子中,添加的函数名仍然是TestNativeFunction
,但C++
反射类中的函数指针是execTestNativeFunction
——也就是生成的函数跳板。
这里有个非常巧妙的设计:当我们在蓝图中没有重写该函数时,编译过程中蓝图不会创建额外的UFunction
(因为如果我们重写了函数,就会创建新的事件或函数图表,无论如何它们都会被当作新的UFunction
编译成字节码)。
但如果我们确实重写了函数,新创建的UFunction
也会被添加到蓝图类的FuncMap
中(而不是C++类)。
为什么说这个设计很聪明?当我们调用FindFunctionByName
尝试获取TestNativeFunction
的函数指针时,实际上是在某个实例上调用。如果这个实例是我们AUHTTest
类型的BP实例(即UBlueprintGeneratedClass
实例且SuperClass
为AUHTTest
),该函数会首先检查实例类的FuncMap
是否有这个函数的映射——如果蓝图实现了该函数,就能找到指向字节码的函数;如果没有,搜索结果为nullptr
,就会递归检查其SuperClass
的FuncMap
(本例中就是AUHTTest
的FuncMap
),找到execTestNativeFunction
函数。最终在ProcessEvent
时,如果是字节码就执行字节码,如果是C++
函数就执行函数跳板代码——后者最终会调用_Implementation
函数。
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
UFunction* UClass::FindFunctionByName(FName InName, EIncludeSuperFlag::Type IncludeSuper) const
{
LLM_SCOPE(ELLMTag::UObject);
UFunction* Result = nullptr;
UE_AUTORTFM_OPEN(
{
UClass* SuperClass = GetSuperClass();
if (IncludeSuper == EIncludeSuperFlag::ExcludeSuper || ( Interfaces.Num() == 0 && SuperClass == nullptr ) )
{
// Trivial case: just look up in this class's function map and don't involve the cache
FUClassFuncScopeReadLock ScopeLock(FuncMapLock);
Result = FuncMap.FindRef(InName);
}
else
{
// Check the cache
bool bFoundInCache = false;
{
FUClassFuncScopeReadLock ScopeLock(AllFunctionsCacheLock);
if (UFunction** SuperResult = AllFunctionsCache.Find(InName))
{
Result = *SuperResult;
bFoundInCache = true;
}
}
if (!bFoundInCache)
{
// Try this class's FuncMap first
{
FUClassFuncScopeReadLock ScopeLock(FuncMapLock);
Result = FuncMap.FindRef(InName);
}
if (Result)
{
// Cache the result
FUClassFuncScopeWriteLock ScopeLock(AllFunctionsCacheLock);
AllFunctionsCache.Add(InName, Result);
}
else
{
// Check superclass and interfaces
if (Interfaces.Num() > 0)
{
for (const FImplementedInterface& Inter : Interfaces)
{
Result = Inter.Class ? Inter.Class->FindFunctionByName(InName) : nullptr;
if (Result)
{
break;
}
}
}
if (Result == nullptr && SuperClass != nullptr )
{
Result = SuperClass->FindFunctionByName(InName);
}
{
// Do a final check to make sure the function still doesn't exist in this class before we add it to the cache, in case the function was added by another thread since we last checked
// This avoids us writing null (or a superclass func with the same name) to the cache if the function was just added
FUClassFuncScopeReadLock ScopeLockFuncMap(FuncMapLock);
if (FuncMap.FindRef(InName) == nullptr)
{
// Cache the result (even if it's nullptr)
FUClassFuncScopeWriteLock ScopeLock(AllFunctionsCacheLock);
AllFunctionsCache.Add(InName, Result);
}
}
}
}
}
});
return Result;
}
对于原生函数,会新增一个FNameNativePtrPair
类型的FuncInfo[]
数组。在RegisterFunctions()
调用时,AUHTTest::execTestNativeFunction
(即函数跳板
)的地址会被存入反射类的NativeFunctionLookupTable
。当需要时,我们可以直接从表中按名称查找原生函数地址并直接调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Begin Class AUHTTest
void AUHTTest::StaticRegisterNativesAUHTTest()
{
UClass* Class = AUHTTest::StaticClass();
static const FNameNativePtrPair Funcs[] = {
{ "TestNativeFunction", &AUHTTest::execTestNativeFunction },
};
FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, UE_ARRAY_COUNT(Funcs));
}
// --------------------------Class.h-----------------------------
/** This class's native functions. */
TArray<FNativeFunctionLookup> NativeFunctionLookupTable;
重点总结
本文我们学习了UHT
如何为UCLASS
生成反射代码,以及虚幻引擎中反射系统的工作原理。我们还了解了BlueprintNativeEvent
函数是如何被反射系统处理的。几个关键要点:
UHT
会专门解析头文件并根据宏生成代码。这就是为什么如果我们没有正确标记函数或属性,它们就无法利用反射系统- 反射不是零成本的,如果不需要某些功能,就不应该给每个函数都加
UFUNCTION()
、每个变量都加UPROPERTY()
- 对于
BlueprintNativeEvent
,最好在蓝图中实际实现逻辑- 因为如果我们最终回调到
_Implementation
函数,这就不再是直接的C++
原生调用,而是需要经过反射系统和蓝图虚拟机执行跳板代码,然后才调用_Implementation
函数(而原本可以直接调用该函数避免额外开销)
- 因为如果我们最终回调到
- 在last post的例子中我们提到字节码执行从
ReceiveBeginPlay
开始,这是因为它是BlueprintImplementableEvent
,所以蓝图重写的函数会被添加到FuncMap
并指向字节码中的标签。但即使我们没有蓝图实现也没关系,系统会生成一个空函数并调用它——只是没有跳板逻辑指向_Implementation
函数 - 还有个更高级的
Meta Specifier
叫CustomThunk
,它可以跳过UHT
的函数跳板生成过程,直接创建手动定义的跳板(对于Foo
函数需要DEFINE_FUNCTION(execFoo))
,这样我们就能完全控制BPVM
,比如移动栈指针或操作参数,就像编写高级汇编代码一样