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:
- Runtime/CoreUObject/Public/Object/ObjectMacros.h (UHT markers)
- Runtime/CoreUObject/Public/Object/ScriptMacros.h (mostly
P_*
macros that can retrieve data from theStack
using reflection) - Runtime/CoreUObject/Public/UObject/Class.h (definitions of reflection base classes like
UField
/UEnum
/UStruct
/UClass
)
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 | // This pair of macros is used to help implement GENERATED_BODY() and GENERATED_USTRUCT_BODY() |
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 | // NetActor.h |
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 | # GENERATED_BODY ultimately generates the following string: |
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 | # e.g |
__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 | // GENERATED_BODY |
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 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. |
You see! This generated generated.h
defines the macros mentioned above:
1 | CURRENT_FILE_ID |
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
andGENERATED_UCLASS_BODY
is thatGENERATED_BODY
declares and defines a constructor receivingconst FObjectInitializer&
, whileGENERATED_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 |
After expansion, it becomes:
1 | class ANetActor : public AActor |
You can see it uses:
DECLARE_CLASS
: to declare several key pieces of information about the current class such asSuper
andThisClass
, and also definesStaticClass
/StaticPackage
/StaticClassCastFlags
and overloadednew
;DECLARE_FUNCTION
: to create intermediate functions for functions marked withUFUNCTION
;DECLARE_SERIALIZER
: to overload<<
to allow serialization withFArchive
;DECLARE_VTABLE_PTR_HELPER_CTOR
: to declare a constructor that receivesFVTableHelper&
parameter;DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER_DUMMY
: used forHotReload
, the only usage occurs in the template functionInternalVTableHelperCtorCaller
in Class.h;DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL
: defines a static function named__DefaultConstructor
that callsplacement-new
to create class objects (for uniform memory allocation), the one and only calling location in the engine occurs in the template functionInternalConstructor
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 | class ANetActor : public AActor |
This is the declaration of our ANetActor
class after processing by UHT, where several functions, typedef
s, 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 |
|
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
inGetPrivateStaticClass
are all the methods UHT generates for accessing the metadata of the class based on our declaration. TheUClass
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 | UCLASS() |
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:
- Obtain the
UFunction
object for the specified function usingUObject::FindFunction
(returnsNULL
if the specified function does not have theUFUNCTION
marker); - Use
ProcessEvent
to call the function, where the first parameter is the calling functionUFunction
and the second is the parameter listvoid*
;
1 | { |
Note: The size of
ParamSize
in theUFunction
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 tovoid*
.
For example, a function receiving int32
/bool
/AActor*
three types will have a ParamSize
equal to:
1 | // sizeof(Params) == 16 |
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 | // This macro is used to declare a thunk function in autogenerated boilerplate code |
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 | // This macro is used to declare a thunk function in autogenerated boilerplate code |
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 | // DECLARE_FUNCTION(execSetHp) |
And DECLARE_FUNCTION(execGetHp)
:
1 | // DECLARE_FUNCTION(execGetHp) |
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 | // Runtime/CoreUObject/Public/Script.h |
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 | UCLASS(BlueprintType,Blueprintable) |
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 | UFUNCTION(BlueprintCallable,CustomThunk, meta = (CustomStructureParam = "StructPack")) |
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 | template<> REFLECTIONEXAMPLE_API UClass* StaticClass<class ANetActor>(); |
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.