UE reflection impl analysis: reflection code generation (part 1)

UE反射实现分析:反射代码生成(一)

I previously wrote two articles analyzing the implementation of reflection in UE, introducing the basic concepts of reflection in UE and some of the C++ features it relies on. This article will begin analyzing the specific process of UE’s reflection implementation.

The C++ standard does not have reflection features, and the reflection used in UE is a mechanism based on marker syntax and UHT scanning to generate auxiliary code. As David Wheeler famously said, “All problems in computer science can be solved by another level of indirection,” UHT does just that: it analyzes the marked code before actual compilation and generates real C++ code, collecting metadata regarding reflection types for runtime use.

UHT generates a large amount of code. To avoid organizational confusion in this article, I will mainly discuss the actual C++ code generated in generated.h after UHT processes reflection markers like GENERATED_BODY and UFUNCTION.

The code generated by UHT is located in generated.h and gen.cpp. The code in generated.h mostly defines some macros added to declared classes through compiler preprocessing to introduce common members, while the code in gen.cpp represents the specific code generated by UHT to describe class reflection information, maintaining separation of declaration and definition.

There was an article about the UE Property System in UE’s Feeds: Unreal Property System(Reflection).

Most of the UHT macro markers related to reflection in UE are defined in the following header files:

Note: Different engine versions may have significant code changes, so it is essential to reference specific engine versions with a focus on analysis methods.

GENERATED_BODY

Every C++ class inherited from UObject or declared as a USTRUCT in UE will have a series of GENERATED_XXXX macros in its class declaration:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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);

#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()

On the surface, it doesn’t seem particularly useful! It merely concatenates a string. However, the truth often has other mysteries. Understanding it can clarify a series of confusing issues encountered while writing code. This section will analyze the role of the GENERATED_ macros.

Consider the following class 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
// NetActor.h

#pragma once
#include "CoreMinimal.h"
#include "NetActor.generated.h"

UCLASS()
class ANetActor : public AActor
{
GENERATED_BODY() // Note this macro is on line eight of NetActor.h
public:
UFUNCTION()
int32 GetHp() const;

UFUNCTION()
void SetHp(int32 pNewHp);
private:
UPROPERTY()
int32 mHP;
};

// NetActor.cpp
#include "NetActor.h"

int32 ANetActor::GetHp() const
{
return mHP;
}

void ANetActor::SetHp(int32 pNewHp)
{
mHP = pNewHp;
}

When we compile, UBT will trigger UHT to generate the NetActor.generated.h and NetActor.gen.cpp files for this class. The *.generated.h and *.gen.cpp files are located in the following path (relative to the project root):

1
Intermediate\Build\Win64\UE4Editor\Inc\{PROJECT_NAME}

The code in NetActor.generated.h is generated by UHT analyzing our NetActor.h, consisting entirely of macro definitions for later use. Before analyzing generated.h, we need to first discuss the distinction between the GENERATED_BODY and GENERATED_UCLASS_BODY macros. From the macro expansion perspective, the difference between GENERATED_BODY and GENERATED_UCLASS_BODY is:

1
2
3
4
# GENERATED_BODY ultimately generates the following string:
{CURRENT_FILE_ID}_{__LINE__}_GENERATED_BODY
# GENERATED_UCLASS_BODY ultimately generates the following string:
{CURRENT_FILE_ID}_{__LINE__}_GENERATED_BODY_LEGACY

Note: The text wrapped in {} consists of other macro components; we are just listing the different forms of two macros.

  • CURRENT_FILE_ID is the folder name where the project is located, combined with the relative path of the source file and _h.
1
2
3
# e.g
# ReflectionExample\Source\ReflectionExample\NetActor.h
ReflectionExample_Source_ReflectionExample_NetActor_h
  • __LINE__ is the line number of this macro within the file, which is the eighth line in the code above.

Thus, the actual strings concatenated by GENERATED_BODY and GENERATED_UCLASS_BODY are:

1
2
3
4
// GENERATED_BODY
ReflectionExample_Source_ReflectionExample_NetActor_h_8_GENERATED_BODY
// GENERATED_UCLASS_BODY
ReflectionExample_Source_ReflectionExample_NetActor_h_8_GENERATED_BODY_LEGACY

After all this, what is the purpose of these long concatenated strings?

At this point, when we open our NetActor.generated.h file, we can see a lot of macro definitions:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
/*===========================================================================
Generated code exported from UnrealHeaderTool.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/

#include "UObject/ObjectMacros.h"
#include "UObject/ScriptMacros.h"

PRAGMA_DISABLE_DEPRECATION_WARNINGS
#ifdef REFLECTIONEXAMPLE_NetActor_generated_h
#error "NetActor.generated.h already included, missing '#pragma once' in NetActor.h"
#endif
#define REFLECTIONEXAMPLE_NetActor_generated_h

#define ReflectionExample_Source_ReflectionExample_NetActor_h_8_RPC_WRAPPERS \
\
DECLARE_FUNCTION(execSetHp) \
{ \
P_GET_PROPERTY(UIntProperty,Z_Param_pNewHp); \
P_FINISH; \
P_NATIVE_BEGIN; \
P_THIS->SetHp(Z_Param_pNewHp); \
P_NATIVE_END; \
} \
\
DECLARE_FUNCTION(execGetHp) \
{ \
P_FINISH; \
P_NATIVE_BEGIN; \
*(int32*)Z_Param__Result=P_THIS->GetHp(); \
P_NATIVE_END; \
}

#define ReflectionExample_Source_ReflectionExample_NetActor_h_8_RPC_WRAPPERS_NO_PURE_DECLS \
\
DECLARE_FUNCTION(execSetHp) \
{ \
P_GET_PROPERTY(UIntProperty,Z_Param_pNewHp); \
P_FINISH; \
P_NATIVE_BEGIN; \
P_THIS->SetHp(Z_Param_pNewHp); \
P_NATIVE_END; \
} \
\
DECLARE_FUNCTION(execGetHp) \
{ \
P_FINISH; \
P_NATIVE_BEGIN; \
*(int32*)Z_Param__Result=P_THIS->GetHp(); \
P_NATIVE_END; \
}

#define ReflectionExample_Source_ReflectionExample_NetActor_h_8_INCLASS_NO_PURE_DECLS \
private: \
static void StaticRegisterNativesANetActor(); \
friend struct Z_Construct_UClass_ANetActor_Statics; \
public: \
DECLARE_CLASS(ANetActor, AActor, COMPILED_IN_FLAGS(CLASS_Abstract), CASTCLASS_None, TEXT("/Script/ReflectionExample"), NO_API) \
DECLARE_SERIALIZER(ANetActor)

#define ReflectionExample_Source_ReflectionExample_NetActor_h_8_INCLASS \
private: \
static void StaticRegisterNativesANetActor(); \
friend struct Z_Construct_UClass_ANetActor_Statics; \
public: \
DECLARE_CLASS(ANetActor, AActor, COMPILED_IN_FLAGS(CLASS_Abstract), CASTCLASS_None, TEXT("/Script/ReflectionExample"), NO_API) \
DECLARE_SERIALIZER(ANetActor)

#define ReflectionExample_Source_ReflectionExample_NetActor_h_8_STANDARD_CONSTRUCTORS \
/** Standard constructor, called after all reflected properties have been initialized */ \
NO_API ANetActor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); \
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(ANetActor) \
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, ANetActor); \
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(ANetActor); \
private: \
/** Private move- and copy-constructors, should never be used */ \
NO_API ANetActor(ANetActor&&); \
NO_API ANetActor(const ANetActor&); \
public:

#define ReflectionExample_Source_ReflectionExample_NetActor_h_8_ENHANCED_CONSTRUCTORS \
/** Standard constructor, called after all reflected properties have been initialized */ \
NO_API ANetActor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; \
private: \
/** Private move- and copy-constructors, should never be used */ \
NO_API ANetActor(ANetActor&&); \
NO_API ANetActor(const ANetActor&); \
public: \
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, ANetActor); \
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(ANetActor); \
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(ANetActor)

#define ReflectionExample_Source_ReflectionExample_NetActor_h_8_PRIVATE_PROPERTY_OFFSET \
FORCEINLINE static uint32 __PPO__mHP() { return STRUCT_OFFSET(ANetActor, mHP); }

#define ReflectionExample_Source_ReflectionExample_NetActor_h_5_PROLOG
#define ReflectionExample_Source_ReflectionExample_NetActor_h_8_GENERATED_BODY_LEGACY \
PRAGMA_DISABLE_DEPRECATION_WARNINGS \
public: \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_PRIVATE_PROPERTY_OFFSET \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_RPC_WRAPPERS \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_INCLASS \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_STANDARD_CONSTRUCTORS \
public: \
PRAGMA_ENABLE_DEPRECATION_WARNINGS

#define ReflectionExample_Source_ReflectionExample_NetActor_h_8_GENERATED_BODY \
PRAGMA_DISABLE_DEPRECATION_WARNINGS \
public: \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_PRIVATE_PROPERTY_OFFSET \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_RPC_WRAPPERS_NO_PURE_DECLS \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_INCLASS_NO_PURE_DECLS \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_ENHANCED_CONSTRUCTORS \
private: \
PRAGMA_ENABLE_DEPRECATION_WARNINGS

template<> REFLECTIONEXAMPLE_API UClass* StaticClass<class ANetActor>();

#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID ReflectionExample_Source_ReflectionExample_NetActor_h

PRAGMA_ENABLE_DEPRECATION_WARNINGS

You see! This generated generated.h defines the macros mentioned above:

1
2
3
4
5
CURRENT_FILE_ID
// GENERATED_BODY
ReflectionExample_Source_ReflectionExample_NetActor_h_8_GENERATED_BODY
// GENERATED_UCLASS_BODY
ReflectionExample_Source_ReflectionExample_NetActor_h_8_GENERATED_BODY_LEGACY

Since our NetActor.h includes the NetActor.generated.h header file, when actual compilation occurs, the GENERATED_BODY macro will be expanded, resulting in the code that is the outcome of the macro ReflectionExample_Source_ReflectionExample_NetActor_h_8_GENERATED_BODY found in NetActor.generated.h.

Because I used GENERATED_BODY in the NetActor.h, I will first analyze the real code after this macro is expanded.

Actually, the difference between GENERATED_BODY and GENERATED_UCLASS_BODY is that GENERATED_BODY declares and defines a constructor receiving const FObjectInitializer&, while GENERATED_UCLASS_BODY only declares this constructor and requires the user to provide a definition.

1
ANetActor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

The real macro name of GENERATED_BODY is wrapped inside another layer of macros:

1
2
3
4
5
6
7
8
9
#define ReflectionExample_Source_ReflectionExample_NetActor_h_8_GENERATED_BODY \
PRAGMA_DISABLE_DEPRECATION_WARNINGS \
public: \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_PRIVATE_PROPERTY_OFFSET \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_RPC_WRAPPERS_NO_PURE_DECLS \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_INCLASS_NO_PURE_DECLS \
ReflectionExample_Source_ReflectionExample_NetActor_h_8_ENHANCED_CONSTRUCTORS \
private: \
PRAGMA_ENABLE_DEPRECATION_WARNINGS

After expansion, it becomes:

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
class ANetActor : public AActor
{
DECLARE_FUNCTION(execSetHp)
{
P_GET_PROPERTY(UIntProperty,Z_Param_pNewHp);
P_FINISH;
P_NATIVE_BEGIN;
P_THIS->SetHp(Z_Param_pNewHp);
P_NATIVE_END;
}

DECLARE_FUNCTION(execGetHp)
{
P_FINISH;
P_NATIVE_BEGIN;
*(int32*)Z_Param__Result = P_THIS->GetHp();
P_NATIVE_END;
}
private:
static void StaticRegisterNativesANetActor();
friend struct Z_Construct_UClass_ANetActor_Statics;
public:
DECLARE_CLASS(ANetActor, AActor, COMPILED_IN_FLAGS(0), CASTCLASS_None, TEXT("/Script/ReflectionExample"), NO_API)
DECLARE_SERIALIZER(ANetActor)

/** Standard constructor, called after all reflected properties have been initialized */
NO_API ANetActor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { };
private:
/** Private move- and copy-constructors, should never be used */
NO_API ANetActor(ANetActor&&);
NO_API ANetActor(const ANetActor&);
public:
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, ANetActor);
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(ANetActor);
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(ANetActor)

public:
UFUNCTION() // Note that these are all empty macros.
int32 GetHp() const;
UFUNCTION() // Note that these are all empty macros.
void SetHp(int32 pNewHp);
private:
UPROPERTY() // Note that these are all empty macros.
int32 mHP;
};

You can see it uses:

  • DECLARE_CLASS: to declare several key pieces of information about the current class such as Super and ThisClass, and also defines StaticClass/StaticPackage/StaticClassCastFlags and overloaded new;
  • DECLARE_FUNCTION: to create intermediate functions for functions marked with UFUNCTION;
  • DECLARE_SERIALIZER: to overload << to allow serialization with FArchive;
  • DECLARE_VTABLE_PTR_HELPER_CTOR: to declare a constructor that receives FVTableHelper& parameter;
  • DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER_DUMMY: used for HotReload, the only usage occurs in the template function InternalVTableHelperCtorCaller in Class.h;
  • DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL: defines a static function named __DefaultConstructor that calls placement-new to create class objects (for uniform memory allocation), the one and only calling location in the engine occurs in the template function InternalConstructor in Class.h;

Since we didn’t mark the ANetActor class with XXXX_API, it won’t be exported. The constructor used in the UHT-generated class ANetActor is NO_API.

Moreover, since macros like UFUNCTION are empty macros in C++ definitions, they cannot strictly be called macros; they are merely markers for UHT meant for parsing to generate the .generated.h and .gen.cpp code. Therefore, after UHT is executed, they essentially don’t exist for C++ and the compiler (after preprocessing, they are completely absent).

These several macros (which become real C++ macros after UHT) can be found defined in CoreUObject/Public/UObject/ObjectMacros.h.

Expanding all the above macros yields:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
class ANetActor : public AActor
{
// DECLARE_FUNCTION won’t be elaborated upon in this section, to conserve space for next section, so I’ll leave the macro.
DECLARE_FUNCTION(execSetHp)
{
P_GET_PROPERTY(UIntProperty,Z_Param_pNewHp);
P_FINISH;
P_NATIVE_BEGIN;
P_THIS->SetHp(Z_Param_pNewHp);
P_NATIVE_END;
}

DECLARE_FUNCTION(execGetHp)
{
P_FINISH;
P_NATIVE_BEGIN;
*(int32*)Z_Param__Result = P_THIS->GetHp();
P_NATIVE_END;
}

private:
static void StaticRegisterNativesANetActor();
friend struct Z_Construct_UClass_ANetActor_Statics;
private:
ANetActor& operator=(ANetActor&&);
ANetActor& operator=(const ANetActor&);
NO_API static UClass* GetPrivateStaticClass();

public:
// DECLARE_CLASS(ANetActor, AActor, COMPILED_IN_FLAGS(0), CASTCLASS_None, TEXT("/Script/ReflectionExample"), NO_API)
/** Bitwise union of #EClassFlags pertaining to this class.*/
enum {StaticClassFlags=COMPILED_IN_FLAGS(0)};
/** Typedef for the base class ({{ typedef-type }}) */
typedef AActor Super;
/** Typedef for {{ typedef-type }}. */
typedef ANetActor 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/ReflectionExample");
}
/** 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;
}

// DECLARE_SERIALIZER(ANetActor)
friend FArchive &operator<<( FArchive& Ar, ANetActor*& Res )
{
return Ar << (UObject*&)Res;
}
friend void operator<<(FStructuredArchive::FSlot InSlot, ANetActor*& Res)
{
InSlot << (UObject*&)Res;
}

/** Standard constructor, called after all reflected properties have been initialized */
NO_API ANetActor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { };
private:
/** Private move- and copy-constructors, should never be used */
NO_API ANetActor(ANetActor&&);

NO_API ANetActor(const ANetActor&);
public:
// DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, ANetActor);
static UObject* __VTableCtorCaller(FVTableHelper& Helper)
{
return nullptr;
}
// DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(ANetActor);
/** DO NOT USE. This constructor is for internal usage only for hot-reload purposes. */ \
API ANetActor(FVTableHelper& Helper);

// DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(ANetActor)
static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj()) ANetActor(X); }

public:
UFUNCTION() // Note that all these are empty macros.
int32 GetHp() const;
UFUNCTION() // Note that all these are empty macros.
void SetHp(int32 pNewHp);
private:
UPROPERTY() // Note that all these are empty macros.
int32 mHP;
};

This is the declaration of our ANetActor class after processing by UHT, where several functions, typedefs, serialization, and memory allocation have been defined.

Also, similar to C#, Super is actually defined as a typedef for our class, allowing UE to add common access functions, supporting UE’s object system through this form.

GetPrivateStaticClass

The GetPrivateStaticClass implemented in NetActor.gen.cpp is defined using the IMPLEMENT_CLASS macro (this series of IMPLEMENT_ macros is also defined in Class.h):

1
IMPLEMENT_CLASS(ANetActor, 2260007263);

After expansion, it becomes:

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
#define IMPLEMENT_CLASS(ANetActor, 2260007263)
static ANetActorCompiledInDefer<ANetActor> AutoInitializeANetActor(TEXT("ANetActor"), sizeof(ANetActor), 2260007263);
UClass* ANetActor::GetPrivateStaticClass()
{
static UClass* PrivateStaticClass = NULL;
if (!PrivateStaticClass)
{
// GetPrivateStaticClassBody is a Template function, Helper template allocate and construct a UClass
/* this could be handled with templates, but we want it external to avoid code bloat */
GetPrivateStaticClassBody(
StaticPackage(),
(TCHAR*)TEXT("ANetActor") + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0),
PrivateStaticClass,
StaticRegisterNativesANetActor,
sizeof(ANetActor),
alignof(ANetActor),
(EClassFlags)ANetActor::StaticClassFlags,
ANetActor::StaticClassCastFlags(),
ANetActor::StaticConfigName(),
(UClass::ClassConstructorType)InternalConstructor<ANetActor>,
(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<ANetActor>,
&ANetActor::AddReferencedObjects,
&ANetActor::Super::StaticClass,
&ANetActor::WithinClass::StaticClass
);
}
return PrivateStaticClass;
}

GetPrivateStaticClass (defined in Class.cpp) constructs a UClass object from the information of the current class. It is a singleton object, which can be accessed through UXXX::StaticClass().

Note: The parameters passed to GetPrivateStaticClassBody in GetPrivateStaticClass are all the methods UHT generates for accessing the metadata of the class based on our declaration. The UClass stored in it is the metadata of our defined class, and not every defined class generates a unique UClass; instead, each class produces a different UClass instance.

UFUNCTION

In UE, any function that needs to allow reflection must include the UFUNCTION() marker.

1
2
3
4
5
6
7
8
UCLASS()
class ANetActor : public AActor
{
GENERATED_BODY()
public:
UFUNCTION()
void SetHp(int32 pNewHp);
};

UHT scans all functions marked with UFUNCTION, producing a definition for an intermediary function named execFUNC_NAME (called a thunk function). This standardizes the calling rules for all UFUNCTION function calls (this/calling parameters and return values), wrapping the real function to be executed.

This function can then be invoked through reflection:

  1. Obtain the UFunction object for the specified function using UObject::FindFunction (returns NULL if the specified function does not have the UFUNCTION marker);
  2. Use ProcessEvent to call the function, where the first parameter is the calling function UFunction and the second is the parameter list void*;
1
2
3
4
5
6
7
8
9
10
11
{
UFunction* funcSetHp = pNetActor->FindFunctionChecked("SetHp");
if (funcSetHp)
{
// struct define in scope
struct funcSetHpParams { int32 NewHp; } InsParam;
InsParam.NewHp = 123;
// call SetHp
ProcessEvent(funcSetHp, (void*)(&InsParam));
}
}

Note: The size of ParamSize in the UFunction object equals the size of the structure of all members and is byte-aligned. Therefore, all parameters can be encapsulated into one structure and converted to void*.

For example, a function receiving int32/bool/AActor* three types will have a ParamSize equal to:

1
2
3
4
5
6
// sizeof(Params) == 16
struct Params {
int32 pIval;
bool pBool;
AActor* pPointer;
};

Relevant content about memory alignment can be found in my previous article: Memory Alignment Issues in Struct Member

DECLARE_FUNCTION

The macro definition of DECLARE_FUNCTION is:

1
2
3
4
5
// This macro is used to declare a thunk function in autogenerated boilerplate code
#define DECLARE_FUNCTION(func) static void func( UObject* Context, FFrame& Stack, RESULT_DECL )

// This macro is used to define a thunk function in autogenerated boilerplate code
#define DEFINE_FUNCTION(func) void func( UObject* Context, FFrame& Stack, RESULT_DECL )

From the code we manually parsed above, we can see that for functions marked with UFUNCTION, UHT generates a DECLARE_FUNCTION macro during parsing, which is defined as:

1
2
3
4
// This macro is used to declare a thunk function in autogenerated boilerplate code
#define DECLARE_FUNCTION(func) static void func( UObject* Context, FFrame& Stack, RESULT_DECL )
// This macro is used to define a thunk function in autogenerated boilerplate code
#define DEFINE_FUNCTION(func) void func( UObject* Context, FFrame& Stack, RESULT_DECL )

As mentioned in my previous article, there is essentially no difference between C++ member functions and non-member functions; the only distinction is that C++ member functions have an implicit this pointer parameter. This is similarly handled by DECLARE_FUNCTION, which allows members and non-members to be unified in this form, where Context naturally represents the traditional C++ implicit this pointer, indicating the object that is currently calling the member function.

Note: In older engine versions (before 4.18.3), there was no Context parameter, which was supported starting from 4.19.

Now, let’s expand the DECLARE_FUNCTION(execSetHp) from NetActor.generated.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// DECLARE_FUNCTION(execSetHp)
void execSetHp( UObject* Context, FFrame& Stack, RESULT_DECL )
{
// P_GET_PROPERTY(UIntProperty,Z_Param_pNewHp);
UIntProperty::TCppType Z_Param_pNewHp = UIntProperty::GetDefaultPropertyValue();
Stack.StepCompiledIn<UIntProperty>(&Z_Param_pNewHp);

// P_FINISH;
Stack.Code += !!Stack.Code; /* increment the code ptr unless it is null */

// P_NATIVE_BEGIN;
{ SCOPED_SCRIPT_NATIVE_TIMER(ScopedNativeCallTimer);

// P_THIS->SetHp(Z_Param_pNewHp);
((ThisClass*)(Context))->SetHp(Z_Param_pNewHp);

// P_NATIVE_END;
}
}

And DECLARE_FUNCTION(execGetHp):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// DECLARE_FUNCTION(execGetHp)
void execGetHp( UObject* Context, FFrame& Stack, RESULT_DECL )
{
// P_FINISH;
Stack.Code += !!Stack.Code; /* increment the code ptr unless it is null */

// P_NATIVE_BEGIN;
{ SCOPED_SCRIPT_NATIVE_TIMER(ScopedNativeCallTimer);

// *(int32*)Z_Param__Result=P_THIS->GetHp();
*(int32*)Z_Param__Result=((ThisClass*)(Context))->GetHp();

// P_NATIVE_END;
}
}

These macros starting with P_ encapsulate the logic to retrieve the actual function parameters from the Context and Stack. They are defined in Runtime/CoreUObject/Public/Object/ScriptMacros.h.

The RESULT_DECL macro is defined in Script.h:

1
2
3
4
5
6
// Runtime/CoreUObject/Public/Script.h
//
// Blueprint VM intrinsic return value declaration.
//
#define RESULT_PARAM Z_Param__Result
#define RESULT_DECL void*const RESULT_PARAM

When expanded, it becomes:

1
void*const Z_Param__Result

It is a top-level const, meaning the pointer value cannot be modified, but the value it points to can be modified, used for handling function return values.### Custom Thunk Function

As mentioned above, when we mark a function with UFUNCTION, UHT automatically generates a function named execFunc. If we don’t want UHT to generate this Thunk function, we can use CustomThunk to mark it and provide our own.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UCLASS(BlueprintType,Blueprintable)
class ANetActor:public AActor
{
GENERATED_BODY()
public:
UFUNCTION(CustomThunk)
int32 GetHp()const;
DECLARE_FUNCTION(execGetHp)
{
P_FINISH;
P_NATIVE_BEGIN;
*(int32*)Z_Param__Result = P_THIS->GetHp();
P_NATIVE_END;
}
};

This means we need to write DECLARE_FUNCTION ourselves to handle the logic passed through ProcessEvent. For example, consider implementing a generic function that allows any struct with reflection (whether from Blueprint or C++) to be serialized to JSON. To achieve such functionality, we need to write the logic ourselves in the Thunk function.

1
2
3
4
5
6
7
UFUNCTION(BlueprintCallable,CustomThunk, meta = (CustomStructureParam = "StructPack"))
FString StructToJson(const FNetActorStruct& StructPack);

DECLARE_FUNCTION(execStructToJson)
{
// ...
}

The CustomStructureParam in meta means the parameter can be treated as a wildcard, allowing any type of parameter to be passed.

UPROPERTY

Adding the UPROPERTY marker to properties within a class will not generate extra code in generated.h, but it will generate its reflection information code in gen.cpp. The details of the code generated in gen.cpp will be covered in detail in the next article.

StaticClass/Struct/Enum

In generated.h, UHT generates corresponding Static*<> template specializations for the reflection types (UObject class/struct/enum) declared in the current file:

1
2
3
template<> REFLECTIONEXAMPLE_API UClass* StaticClass<class ANetActor>();
template<> REFLECTIONEXAMPLE_API UEnum* StaticEnum<ENetEnum>();
template<> REFLECTIONEXAMPLE_API UScriptStruct* StaticStruct<struct FNetStruct>();

This is the way we access UClass/UStruct/UEnum at runtime using StaticClass<ANetActor>.

This form was added after UE4.21; before 4.21, you had to use the following method:

1
UEnum* FoundEnum = FindObject<UEnum>(ANY_PACKAGE, *EnumTypeName, true); 

The definition of Static*<>() can be found in gen.cpp.

End

In summary, the implementation of UE reflection involves: generating reflection metadata with UHT and constructing the corresponding UClass/UStruct/UEnum at runtime using these metadata, thereby providing support for reflection. This article mainly discusses the code generated by UHT in generated.h. The core aspect is that UHT adds a set of common members to the declarations of reflection classes, relying on these members to support UE’s object system management.

The next article will focus on the code generated in gen.cpp, which truly records the object information of reflection classes, such as the names of reflective members, function addresses, parameter passing, data member class offsets, etc. By analyzing them, we can understand what information we can obtain about a class through reflection.

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE reflection impl analysis: reflection code generation (part 1)
Author:LIPENGZHA
Publish Date:2021/03/10 15:14
Word Count:10k Words
Link:https://en.imzlp.com/posts/9780/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!