But what is UClass? From GENERATED_BODY to BlueprintNativeEvent
If you wrote a Unreal Game in C++, you know what BlueprintNativeEvent is, it allows C++ to have a default implementation of a function, and then have that function overridden in Blueprint. But how does this achieved? This post will explain how Unreal Header Tool (UHT) wield the magic and generates the thunk function for BlueprintNativeEvent.
The content in this post is based on Unreal Engine 5.4.0
If I made a mistake, please comment below and help future readers!
Preface
Pretty much any first “Hello, world!” project in Unreal Engine C++ learning material will go through the various meta specifiers of a function, unavoidably touching a few famous ones like BlueprintCallable
, BlueprintPure
, BlueprintImplementableEvent
, and BlueprintNativeEvent
.
The first two are pretty straight forward. The third one is a bit fancy, it allows the blueprint to create a function’s implementation rather than a native C++ version. The last one, BlueprintNativeEvent
, is the most magical. It allows C++ to have a default implementation of a function, and then have that function overridden in Blueprint. During execution, it’s just gonna magically call the Blueprint implementation if it exists, and fallback to C++ implementation if it doesn’t.
For a custom function void Foo()
we just need to give it a UFUNCTION(BlueprintNativeEvent)
specifier, and then we can implement the function in C++ as void Foo_Implementation()
. Anyone calling to Foo()
without a blueprint override exist will automatically wired to call Foo_Implementation()
. It’s such an intuitive, and natural way of development. The same with UClass
, we create it, and then write the business logic right away, compile, build and everything works smooth like butter.
It’s so smooth that we merely forget how much hassle it takes to write such thing in a native C++ environment. Unfortunately we haven’t invented magic, hence someone must have done the dirty work for us. That someone here, is called Unreal Header Tool (UHT)
.
UHT
In the series From Blueprint To Bytecode, we went through the compilation process of a UBlueprint
. However, in order to create a UBlueprint
object, we must assign a valid class to be its “parent class” in editor, and in order to have a valid class assigned in editor, the project needs to be built and launched. (Duh, how can we create a UBlueprint
object if the source doesn’t even compile and the editor not launch-able?)
We are mainly focusing on creating a
UBlueprint
of a custom C++ class, if the project is a Blueprint-only project. Then this post is irrelevant.
So yes, we are still talking about compilation, except this time, we are looking at the compilation of the source code, not a UBlueprint
. This process exists before a valid unreal editor process is launched.
Just like FKismetCompilerContext
is responsible for compiling a UBlueprint
to UBlueprintGeneratedClass
, the Unreal Header Tool (UHT)
is responsible for generating the C++ code for the Unreal Engine reflection system. It parses each line of our header file, and generates extra C++ code to {name}.generated.h
and {name}.gen.cpp
, then put them at Intermediate/Build/{Platform}/UnrealEditor/Inc/{Project}/UHT/
.
UHT
is essentially a C++ analyzer and parser, it just generate the boilerplate code, not actually compile it. Meaning the compilation may still fail if we didn’t fulfill the information required by the reflection system.
Afterwards, Unreal Build Tool (UBT)
is responsible for build and compile the generated C++ code for different platforms.
Create a test class
Let’s just create a simple class and see them in action, here I’m using JetBrains Rider
, it provides quick creation for a new class, I named it UHTTest
inherited from AActor
. And the header looks like this:
#include "CoreMinimal.h"
is the standard include for Unreal Engine.- It provides the basic types, macros, templates, math functions, and so on.
#include "GameFramework/Actor.h"
is the base class of ourUHTTest
class.#include "UHTTest.generated.h"
is the generated header file byUHT
.- But if we try to navigate to it, it says the file doesn’t exist, because upon creation, this file is not generated yet.
UCLASS()
is a macro that tellsUHT
to generate reflection code for this class.GENERATED_BODY()
is a macro that tellsUHT
to generate reflection code for this class.- Unreal Engine replaces this with all the necessary boilerplate code that gets generated for the class.
- The
GENERATED_BODY
macro takes no arguments, but sets up the class to support the infrastructure required by the engine. It is required for allUCLASS
. - Note that the
GENERATED_BODY
macro must be placed at the very beginning of the class body.
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() Macro
Some may call Unreal C++ as U++ (Unreal++), because it’s not just plain C++. It’s C++ with a lot of extra features and macros. Even with a good implementation of reflections, make it feel convenient like C# but still have the full-fledged C++ power.
In our case above. GENERATED_BODY()
macro is the first one that doesn’t feel like native C++, the definition looks like this:
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);
It’s reversely defined because Macro is sensitive to the order. More preliminary macros are defined first.
In short, GENERATED_BODY()
gets expand to a macro named {CURRENT_FILE_ID}_{__LINE_NUMBER__}_GENERATED_BODY
, but then where’s its definition? Well it doesn’t exist yet, because it’s generated by UHT
and we haven’t compiled it!
Compile and Build
Time to build the project and see the magic happen. After the build is done, we can navigate to the included UHTTest.generated.h
file located at /Intermediate/Build/{Platform}/UnrealEditor/Inc/{Project}/UHT
and inspect the generated header.
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
What and why
What does UHT do
We know that the whole purpose of this huge file is to generate the boilerplate code for us, but let’s face the elephant in the room:
- Why do we even need
UHT
at the first place? What kind of problem does it solve?
The answer is Reflection
. Unreal Engine needs to know the class information at runtime, so that we can spawn objects, serialize them, and so on. But native C++ doesn’t have full reflection support (We do have RTTI but that’s barely sufficient for game). Think about this scenario, when we picked a class in the editor, and drag it into the scene, how do we implement it in native C++ to create an instance of it right away? It’s not trivial, because we don’t know the class information, upon drop the class (At this moment it’s just a thumbnail rendered following mouse cursor) We should at least retrieve information about what class the thumbnail mapped to, as well as what class constructor should we call. The same with invoking a function in a class with name, we at least need to know that there’s a function address in the class instance that have been mapped to the function name questioning. This is what UHT
does, it generates the necessary information of the class and registered them for Unreal Engine to work with the class at runtime.
When do we collect the class information
The next question is:
- When would be a good time to register the class information?
Since static object are initialized before main()
function, it’s not a crazy idea to prepare the class information before main()
function is called. To achieve this, we need to create a static object, and call corresponding functions to initialize it during construct. This is called “Static Auto Registration”, e.g:
1
2
3
4
5
6
7
8
9
struct StaticClassFoo
{
StaticClassFoo()
{
RegisterClass(Foo::StaticClass());
}
}
static StaticClassFoo AutoRegisteredFoo;
Before main()
, the AutoRegisteredFoo
object will be created since it’s a static object, hence calling constructor, which calls RegisterClass()
, by doing so, we can let StaticClassFoo
to handle registration automatically on it’s own. We will touch that later.
Generated.h Breakdown
Back to the code, let’s focus on the GENERATED_BODY()
part. The following code will be generated for each UCLASS()
that has GENERATED_BODY()
macro (GENERATED_BODY()
is mandatory for a UClass
to work anyways), the {Filepath} include the path of the file on local disk, and FID_{Filepath}
is defined as CURRENT_FILE_ID
:
1
#define CURRENT_FILE_ID FID_{Filepath}
What about the small “12” in the source? It’s the line number of the GENERATED_BODY()
macro in the header file!
Swapping them with each bits, we get:
- A:
{CURRENT_FILE_ID}
- B:
_
- C:
12
- D:
_GENERATED_BODY
Look back to the GENERATED_BODY
. It get’s replaced to {CURRENT_FILE_ID}_{__LINE__}_GENERATED_BODY
in the actual header file, which is still a macro waiting to be expanded. And then UHT
generates it’s definition that expaned to {CURRENT_FILE_ID}_12_INCLASS_NO_PURE_DECLS
and {CURRENT_FILE_ID}_12_ENHANCED_CONSTRUCTORS
in the UHTTest.generated.h
file, which is included in the UHTTest.h
, effectively expands GENERATED_BODY
.
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 Implementation
Let’s dive into the UHT
source and see what’s happening there. UHT
is a C# project located at Engine/Source/Programs/Shared/EpicGames.UHT/EpicGames.UHT.csproj
. And we can find the whole process at the public void Generate(IUhtExportFactory factory)
of UhtHeaderCodeGeneratorCppFile.cs
Eventually this function will call private StringBuilder AppendClass(StringBuilder builder, UhtClass classObj)
which then calls using UhtMacroCreator macro = new(builder, this, classObj, GeneratedBodyMacroSuffix);
Finally, AppendMacroName
will be called to generate the macro name. Which will append fileId
then lineNumber
then macroSuffix
to the StringBuilder
.
For a class, the UhtMacroCreator
is created with parameter GeneratedBodyMacroSuffix
as macroSuffix
, which is defined as public const string GeneratedBodyMacroSuffix = "GENERATED_BODY";
So by parsing our header file and extract these variables out, UHT
can create a macro named with {CURRENT_FILE_ID}_{__LINE__}_GENERATED_BODY
, exactly what we were seeing above.
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
}
Yes, the word “Haeder” in codebase
UhtHaederCodeGeneratorStringBuilderExtensions
looks like a typo to me. But hey, nothing is perfect :D
For a more detailed explanation of the
UHT
process, reader can refer to the post How Unreal Macro Generated and this excellent series by Epic employee: InsideUE4 (This is a series from a Chinese platform written in Chinese, so a translation tool may be needed)
Expand GENERATED_BODY()
A lot of macros are expanded here, long story short, we can expand the header to the following form, which suddenly feels a lot close to the C++ code that we all hate XD:
This is pseudo code, because I just manually expand them without replacing each parameters passed to macro. Though it’s close enough to the actual code.
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();
};
Intermediate Checkpoint
This is pretty much all a minimal .generated.h
has to offer. Of course when we add more fancy stuff into the class, new magics will happen, like UENUM
, USTRUCT
, UFUNCTION
, UPROPERTY
, etc. But the idea is the similar. UHT
will generate the necessary boilerplate code for the class to work with Unreal Engine reflection system.
Moving forward, since we already know the .generated.h
is creating the function declarations, then just like native C++, we need to actually define the functions. This is done by UHT
as well, in the .gen.cpp
file.
Gen.cpp breakdown
Includes and Cross Module References
The first part of the code looks like this:
1
2
3
#include "UObject/GeneratedCppIncludes.h"
#include "UHTTest/Public/UnitTest/UHTTest.h"
PRAGMA_DISABLE_DEPRECATION_WARNINGS
A few simple includes, the UObject/GeneratedCppIncludes.h
acts similar to CoreMinimal.h
, plus a bit more headers:
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
Next, we defined EmptyLinkFunctionForGeneratedCodeUHTTest()
as… well… nothing:
1
void EmptyLinkFunctionForGeneratedCodeUHTTest() {}
During compilation, each
.cpp
file will be compiled into a compile unit. (.o
, or.obj
file). And normally when no other places are referencing any symbols in a.cpp
file, the linker might discard the linking process of this whole unit, namelyDead Code Elimination
. So we need to at least contain a symbol, even it’s empty here, and referencing it from another place to avoid it being optimized by linker. There’re legacy code in the engine that still does this for this purpose, but I’m not sure if this is still necessary today, since seems even without this function, other parts of thegen.cpp
file would still achieve the same purpose. (They are being referenced by other parts of the engine)
Then we have the Cross Module References
, which is a bunch of references that each will construct a class from different modules. For functions to construct the reflection class of AUHTTest()
, the _NoRegister()
version is used to construct a class without registering it to Unreal Object System.
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
Pretty much the rest file here is for reflection. It’s not possible to go through all the details of unreal reflection system in this post, but we can still have a basic understanding of the idea behind.
A more complete breakdown of the architecture can be found in this post InsideUE: Type System Code Generation
Registration
Following the “Static Auto Registration” pattern, we created a static object Z_CompiledInDeferFile_{FileID}_1070479904
. From the source code we can see that the followed parameters will be forwarded to RegisterCompiledInInfo
function. Note that Z_CompiledInDeferFile_{FileId}_Statics::ClassInfo
is also passed in as second parameter that storeds Z_Registration_Info_UClass_AUHTTest
, Z_Construct_UClass_AUHTTest
, AUHTTest::StaticClass
, TEXT("AUHTTest")
. (More on that later)
3405001915U
and1070479904
is a class type hash and a declaration hash
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)...);
}
};
Step into RegisterCompiledInInfo()
function, we can see that besides the housekeeping work, it really just calls FClassDeferredRegistry::AddRegistration()
to add the class registration information, eventually added to TDeferredRegistry::Registrations
array.
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;
}
It’s deferred because the registration is not done yet, it will be processed further later, this is to avoid the registration work halting the main thread and freezing the editor.
Prepare Class Information
We now know that eventually the data are being centralized and registered utilizing the “Static Auto Registration” pattern, so to fulfill this architeture, we should prepare the class information. As mentioned before, there’re really only 3 things we need to consider:
Z_Registration_Info_UClass_AUHTTest
Z_Construct_UClass_AUHTTest
AUHTTest::StaticClass
Z_Registration_Info_UClass_AUHTTest
The class is defined in the macro IMPLEMENT_CLASS_NO_AUTO_REGISTRATION(TClass)
, and then it immediately gets passed into GetPrivateStaticClass()
, another check is done for the InnerSingleton
, and if it’s not initialized, it will call GetPrivateStaticClassBody()
to initialize it.
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; \
}
Step into GetPrivateStaticClassBody()
, we can see that it allocate memory for a UClass
object, and stores the memory address back to the ReturnClass
(since it’s a reference typeUClass*&
), and then construct it with the necessary information with placement new
at the ReturnClass
address. Combine the two snippet together, we can see that the UClass
object is created with the AUHTTest
class information, stored in Z_Registration_Info_UClass_AUHTTest.InnerSingleton
. And the info here is just the skeleton of the class.
The UClass
object is then initialized with the InitializePrivateStaticClass()
function, which sets up the class’s inheritance and other properties.
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
There’s also a ClassDefaultConstructor
parameter in the above function, this is passed in as (UClass::ClassConstructorType)InternalConstructor<TClass>
, which is just a wrapper of __DefaultConstructor
function:
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; }
The function looks scary, new((EInternal*)X.GetObj()) TClass;
is a placement new operator, which constructs an object in the memory pointed by X.GetObj()
. The const FObjectInitializer& X
is a helper class that initializes an object, then GetObj()
returns the object being initialized. This is just a fancy way of calling a class constructor. But why do we need this?
In short, we can’t just take the address of a constructor directly in C++ and store it in a function pointer (a constructor is not a normal function—its signature and call mechanism differ). Unreal, however, needs a way to call class constructors at runtime when spawning objects of arbitrary UCLASS
types (Calling the constructor of a class by address). To achieve this, Unreal creates a tiny function “wrapper” that simply does new(...) TClass;
inside.
When Unreal wants to instantiate an object of the class, it calls this wrapper function, as:
- Constructor Pointers Aren’t Allowed
- We cannot write something like
SomeFuncPtr = &TClass::TClass;
because constructors lack a conventional function signature
- We cannot write something like
- Macro Creates a “Wrapper”
- The macro
DEFINE_DEFAULT_CONSTRUCTOR_CALL(TClass)
defines a static function__DefaultConstructor(const FObjectInitializer&)
, that callsnew((EInternal*)X.GetObj()) TClass
;. That wrapper function behaves like a constructor call but can be referenced by a regular function pointer
- The macro
- Reflection / Spawning
- Unreal’s reflection system (and object spawning code) can store and invoke that static function pointer to dynamically construct new instances of the class at runtime without needing to know the class’s constructor signature. It only knows that if it calls the wrapper, a new instance of
TClass
is created.
- Unreal’s reflection system (and object spawning code) can store and invoke that static function pointer to dynamically construct new instances of the class at runtime without needing to know the class’s constructor signature. It only knows that if it calls the wrapper, a new instance of
With this wrapper, the __DefaultConstructor
can be stored as a function pointer in UClass
’s ClassConstructorType
field.
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
This calls UECodeGen_Private::ConstructUClass()
to construct a new class if haven’t, which is a refactored new system for reflection after UE 4.17
, it streamlined the way of creating a UClass
object (And other types, they all being wrapped into the UECodeGen_Private
namespace).
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;
}
As can be seen above, upon ConstructUClass
, the meta data of AUHTTest
will be wrapped in a FClassParams
struct called Z_Construct_UClass_AUHTTest_Statics::ClassParams
and pass to the function. This includes the properties, functions, interfaces, and other class information, here’s the related code snippet.
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
In the above code, we can see that the first parameter of FClassParams
is ClassNoRegisterFunc
, which is a function pointer that returns a UClass
object. This is the StaticClass
function of the class. It’s expanded from the DECLARE_CLASS
macro, and eventually called GetPrivateStaticClass()
, which we’ve seen before, it was defined in IMPLEMENT_CLASS_NO_AUTO_REGISTRATION
and actually being called here:
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(); \
} \
Reflection System Test
Now that we have a basic understanding of how a UCLASS
is generated by UHT
, let’s add something new to the AUHTTest
class, and see what’s been changed. Here, a new function void TestFunction()
and a new variable int32 TestInt32
is added:
1
2
3
4
5
6
public:
UPROPERTY()
int32 TestInt32;
UFUNCTION()
void TestFunction() {};
Back to our UHTTest.gen.cpp
, we can see the reflection class Z_Construct_UClass_AUHTTest_Statics
is being changed:
- A new
FIntPropertyParams
is added for theTestInt32
variable. - A new
FClassFunctionLinkInfo FuncInfo[]
is added withTestFunction
function as an element. - A new
FPropertyParamsBase* const PropPointers[]
is also added, but haven’t been used yet.
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
};
Moving on, we can see that the Property Data
is actually being added to TestInt32
, which will then be added to the PropPointers
array.:
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
};
Next, we can see that in the reflection class param. Function and prop data are no longer pointing to nullptr
, but to the actual data, that’s why the reflection system can now retrieve the information of the new function and variable:
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
We are finally touching the topic of BlueprintNativeEvent
, a special function specifier, that allows a C++ function to be overridden by a Blueprint function. Without further ado, let’s add a new BlueprintNativeEvent
function to our AUHTTest
class:
1
2
3
public:
UFUNCTION(BlueprintNativeEvent)
void TestNativeFunction() {};
Let’s build and compile… and… Boom, compile error! Error log is:
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.
What? UHT
has already generated the definition for us? Let’s check the UHTTest.gen.cpp
file, and we will find that the TestNativeFunction
is indeed defined:
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
We can ignore the details in this long code, and keep moving forward. Since the UHT
has generated the definition for us, how about we just remove our definition in the header file? Save and compile… and… Boom! Another compile error pops up:
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)
However, this time it’s a linker error, which usually means that something is calling a symbol that is not defined, from Epic document we would know that the proper syntax for BlueprintNativeEvent
is to have a _Implementation
function defined in the header file, so this must be the missing symbol, but the question is: Who is calling it? There must be a function call to the _Implementation
function somewhere in the code, otherwise the linker wouldn’t complain about it. Let’s jump back to the UHTTest.gen.cpp
file, and we can find an interesting part:
1
2
3
4
5
6
7
DEFINE_FUNCTION(AUHTTest::execTestNativeFunction)
{
P_FINISH;
P_NATIVE_BEGIN;
P_THIS->TestNativeFunction_Implementation();
P_NATIVE_END;
}
Obviously, this code is calling the _Implementation
function, we are just gonna cut the fancy stuff and jump to the definition: This part of the code is called a Function Thunk
, which is a small piece of code that is generated by the UHT
, and is used by Blueprint Virtual Machine
to interpret and execute. P_FINISH;
here means the end of passing parameters, P_NATIVE_BEGIN
and P_NATIVE_END
is just gonna record an execution time if Script Overhead Stats
debug is active. Then the P_THIS->TestNativeFunction_Implementation();
is the actual call to the _Implementation
function, since we haven’t implemented this function, that’s why the linker is complaining above.
The next question would be, why do we need to go through these hassle than just call the function in C++? To answer this question, let’s first think about another question:
Why not a direct call?
Why don’t we directly call that _Implementation
function in C++? Let’s take a look at this pseudo code:
1
2
3
4
5
6
7
8
// C++ Definition
void AUHTTest::Foo_Implementation()
{
Bar();
}
// Blueprint Override
AUHTTest::Foo() { BP_Bar(); }
The answer is obvious, because calling _Implementation
is usually wrong, calling to _Implementation
function is equivalent to manually call a function’s Super::Func()
version, that’s not always what we want, most of the time, we want to call the bottom-most function (And this override function can determine whether it needs to call its Super
). If child class has override the function, then we want to call the child’s version instead of the parent’s version. So the answer is: unless our intention is calling the parent’s version manually, we always should call Foo()
, no matter in C++ or Blueprint.
What’s the definition?
Since we should always call to Foo()
, then what’s the definition of it? In our case its TestNativeFunction
, so let’s take a look at the UHT
generated code:
1
2
3
4
5
static FName NAME_AUHTTest_TestNativeFunction = FName(TEXT("TestNativeFunction"));
void AUHTTest::TestNativeFunction()
{
ProcessEvent(FindFunctionChecked(NAME_AUHTTest_TestNativeFunction),NULL);
}
Pretty simple, FindFunctionChecked()
will try to get a UFunction
ptr based on the name “TestNativeFunction”, and then ProcessEvent()
will execute the function. In Object.h
, it reads:
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
The rest is pretty straightforward, if the UFunction
is a blueprint function, then it will execute the bytecode generated. If it’s a C++ function, then it will execute the corresponding Function Thunk
code, eventually calling the _Implementation
function.
We are almost there: So how does unreal know whether a BlueprintNativeFunction
has being overridden by a Blueprint? The magic here is ConstructUClass()
and 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;
}
During the class construction, NewClass->CreateLinkAndAddChildFunctionsToMap(Params.FunctionLinkArray, Params.NumFunctions);
will be called. This function just add all the functions to the FuncMap
with their name. After that, we can consider all the functions already has its place in the FuncMap
and can be retrieved by name. In our case. the name of the added function is still TestNativeFunction
, but the function pointer in the C++ reflection class is execTestNativeFunction
, which is the Function Thunk
generated.
Now here’s the really smart thing, when we don’t override the function in Blueprint, during the compilation, no extra UFunction
is being created by blueprint compilation process. (Since if we override the function, a new event or function graph will be created, either way, they will be treated as a new UFunction
and gets compiled to bytecode)
But if we did override the function, the newly created UFunction
will also be added to the ‘FuncMap’ as well for the blueprint class. (Not to the C++ class)
Why is it smart? When we call FindFunctionByName
and try to get the function ptr of TestNativeFunction
, we will call on an instance. If this instance is an BP instance of our ``AUHTTest type. (
UBlueprintGeneratedClass instance with
SuperClass of
AUHTTest), the function will first check if the instance's class's
FuncMap has a map of this function, in this case, if the Blueprint implemented the function, then a function is found and pointing to a bytecode. If not, then the search will yield
nullptr, so it will recursively check it's
SuperClass's
FuncMap (In this case,
AUHTTest's
FuncMap), and find the
execTestNativeFunction function. Eventually, during
ProcessEvent, if it's a bytecode, it calls the bytecode, if it's a C++ function, it calls the
Function Thunk, which ultimately calls the
_Implementation` function.
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;
}
For functions that is a native function. A new array FuncInfo[]
is added with type of FNameNativePtrPair
, then upon RegisterFunctions()
call, the AUHTTest::execTestNativeFunction
(The Function Thunk
) address is stored in the reflection class’s NativeFunctionLookupTable
. When needed, we can just find the native function address by it’s name in the table, and call it directly.:
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;
Take Away
In this post, we have learned how UHT
generates the reflection code for a UCLASS
, and how the reflection system works in Unreal Engine. We have also learned how BlueprintNativeEvent
functions are being handled by the reflection system. A couple take aways:
UHT
specifically parses the header file and generate stuff by the macros. That’s why if we don’t mark a function or property properly, they won’t take advantage of the reflection system.- Reflection is not free, if we don’t need certain feature, we shouldn’t mark every function UFUNCTION() and every variable UPROPERTY().
- For a
BlueprintNativeEvent
, we better actually implement the logic in blueprint- Because if we end up calling back to the
_Implementation
function, it’s no longer a direct native C++ call, instead, it still needs to go through the reflection system, as well as the blueprint VM and execute theThunk
, then calls the_Implementation
function. (Which can be called directly at the first place without extra overhead)
- Because if we end up calling back to the
- In the last post’s example we mentioned the execution of bytecode starts with
ReceiveBeginPlay
, and that’s why, it’s aBlueprintImplementableEvent
, so the blueprint overridden function will be added toFuncMap
and pointing to a label in the bytecode. But we don’t have blueprint implementation, that’s also fine, an empty function will be generated anyway, and being called. Just no thunk logic pointing to a_Implementation
function. - There’s a more advanced meta specifier called
CustomThunk
, it can skip theFunction Thunk
generation process byUHT
, and directly create a manually definedThunk
(forFoo
we needDEFINE_FUNCTION(execFoo)
), in which we can have full control of the BPVM, like moving it’s stack pointer around or operate on the parameters, just like writing a high level assembly code