Reflection refers to the ability of a program to perform self-inspection at runtime, which is very useful in the editor’s property panel, serialization, GC, and other areas. However, C++ does not support reflection features inherently. UE implements the generation of reflection information through UHT based on the syntax of C++, thereby achieving the goal of runtime reflection.
In previous articles, some content related to UE’s build system and reflection was discussed.
Articles related to UE’s build system:
- Build flow of the Unreal Engine4 project
- UE4 Build System: Target and Module
- Differences and Connections Between UEC++ and Standard C++
Articles on using UE’s reflection mechanism for various tricks:
The implementation of UE’s reflection relies on UHT in the build system to perform code generation. This article provides a basic concept introduction to UE’s reflection, and subsequent articles will thoroughly introduce the implementation mechanisms of reflection in UE.
UE’s reflection can achieve Enum reflection (UEnum
), class reflection (UClass
), struct reflection (UStruct
), data member reflection (UProperty
/FProperty
), and member function reflection (UFunction
), allowing access to them at runtime. In fact, calling reflection the Property System would be more appropriate.
Using this reflection information, we can obtain type information about them. This article will take class reflection as an example to introduce UE’s reflection.
Consider the following pure C++ code:
1 | class ClassRef |
How can we obtain information on the data members and functions of the ClassRef
class at runtime?
C++ does not inherently provide such capability. The same requirement in a class created in UE takes the following form:
1 |
|
Key points to note include:
- The
RefObject.generated.h
file - The UCLASS marker
- The GENERATED_BODY marker
- The UPROPERTY marker
- The UFUNCTION marker
This article does not delve too deeply into their specific meanings, but subsequent articles will provide detailed analysis.
UCLASS
/USTRUCT
/UFUNCTION
/UPROPERTY
etc. can have many marker values and meta parameters added within ()
, which guide UHT in generating corresponding reflection code. The parameters they support can be found in UE’s documentation:
This method of adding code markers to inform UE’s build system relies on UHT to generate reflection code, which is stored in gen.cpp
. Note that these reflection markers are only used to instruct UHT to generate code; after the C++ pre-processing phase, they are largely empty macros (some are actual C++ macros). This also leads to a limitation of UE’s reflection markers: C++ macros cannot wrap UE’s reflection markers because they execute prior to pre-processing.
Furthermore, UHT adopts a simple and crude keyword matching hard scan, which imposes significant limitations.
For classes inheriting from UObject, reflection information creates a UClass object, allowing runtime access to the type information of the object. Additionally, the reflection data members and reflection member functions within the class will generate corresponding FProperty
and UFunction
objects for runtime access.
The inheritance hierarchy of UClass:
UObjectBase
UObjectBaseUtility
UObject
UField
UStruct
UClass
For classes inheriting from UObject, the UClass instance can be obtained using GetClass()
, but to directly obtain the UClass of a particular type, one can use StaticClass<UObject>
or UObject::StaticClass()
.
UClass records the inheritance relationships, implemented interfaces, various Flags, etc. Specific details can be found by checking the UClass class definition, from which you can access the information about that UObject’s C++ type.
Moreover, at runtime, one can iterate through the reflection properties in UClass using TFieldIterator
:
1 | URefObject::URefObject(const FObjectInitializer& Initializer):Super(Initializer) |
Execution result:
1 | LogTemp: Property Name: ival |
So how do we access them through the reflection information of properties and member functions?
Accessing Data Members
Firstly, the layout of memory for class instances in C++ is fixed at compile time, so the position of a data member in a class is static. C++ has a feature called pointers to class members, essentially describing the offset of the current data member within the class layout. This topic was discussed in my previous article: Pointers to Class Members in C++ Are Not Actual Pointers.
FProperty does something similar; it records the class-level offset information of reflection data members, with UE’s implementation also relying on pointers to members. This part will be emphasized in later articles, but here we will only discuss the usage method.
To obtain values from an object using FProperty, it needs to call FProperty
‘s ContainerPtrToValuePtr
:
1 | for(TFieldIterator<FProperty> PropertyIter(GetClass());PropertyIter;++PropertyIter) |
This achieves the purpose of accessing data members through FProperty. Since a pointer to the data member is obtained, it is also possible to modify its value.
Accessing Member Functions
Accessing functions via reflection is somewhat more complex, as it requires handling parameter passing and return value reception.
It has been mentioned that UE’s reflection member functions generate UFunction
objects. The reflection information about the function resides within it. Since UFUNCTION can only be marked on classes derived from UObject, UE has encapsulated a set of reflection function calling methods based on UObject:
1 | // defined in UObject |
Only two parameters exist: the first is a pointer to the UFunction, and the second is a void*
pointer, serving as a universal means to pass parameters and receive return values.
For functions marked with UFUNCTION, UHT generates a thunk function for that function, typically taking the following form:
1 | DEFINE_FUNCTION(URefObject::execfunc) |
After expanding the DEFINE_FUNCTION
macro, it reveals a fixed prototype:
1 |
|
In ProcessEvent
, the reflection function’s thunk function will be called according to this unified prototype, forwarding it to the actual C++ function, allowing for reflection-based invocation to reach the actual C++ function. You can also prevent UHT from generating the thunk function by adding the CustomThunk
marker in UFUNCTION
, allowing for manual provision to accommodate special needs (e.g., overriding in unlua).
Returning to ProcessEvent
, its function prototype involves three key points:
ProcessEvent
is a member function ofUObject
- The first parameter of
ProcessEvent
must be a UFunction within the current class void* Parms
must be a contiguous memory structure
It is crucial to discuss how to use void* Parms
.
For instance, with the function:
1 | bool func(int32 InIval); |
It receives one parameter and returns a boolean value. How can we do both with a single parameter?
By wrapping them in a structure! As shown in the following format:
1 | struct RefObject_eventFunc_Parms |
The parameters to be passed to the function are placed at the forefront, with the return value as the last data member (if present). The total size of the function’s parameters and return value structure can also be determined at runtime using UFunction::ParmsSize
, allowing for dynamic allocation. Additionally, each parameter’s memory can be accessed through the FProperty
of UFunction
. This part will be discussed in detail in later articles.
Therefore, once we obtain the specified UFunction through UClass
, we can proceed as follows:
1 | for(TFieldIterator<UFunction> PropertyIter(GetClass());PropertyIter;++PropertyIter) |
However, there is one issue with this reflection-based function calling: it can’t handle cases where parameters and return values are passed by reference. For example:
1 | UFUNCTION() |
The reflection information generated for these two functions is identical! (The flag for the FProperty generated for the L parameter will include an additional CPF_OutParm
, while the FProperty for the return value will have CPF_ReturnParm
).
This limitation arises because C++ references must be bound at initialization:
1 | class ClassRef |
This creates a restriction in UHT’s parameter structure. By default, UHT generates the parameter structure for each reflection function by instantiating it and then assigning the actual parameter values to the members of that instance, similar to the previous example, making a copy of the value. But a reference cannot be modified and can only be set at initialization. When calling ProcessEvent, the actual invocation of the C++ function will, if parameters are references, bind to the data members of the UHT-generated structure rather than to the parameters we genuinely passed.
In effect, calling a function based on reflection involves two steps:
- Assigning parameters to the function parameter structure of ProcessEvent
- ProcessEvent then passes the parameter structure to the actual C++ function.
This results in the parameters passed by invoking the UFunction through ProcessEvent
merely being copies of the original parameters, rather than truly binding the reference relationships (because all parameters will be assigned to the UHT-created parameter structure before reaching ProcessEvent). Thus, modifications to reference parameters made in the actual C++ function affect only the instance of the UHT-generated parameter structure, not the truly passed parameters.
This article provided a simple introduction to UE’s reflection and demonstrated how to access data members and member functions via reflection information obtained through UClass
. A more detailed exploration of reflection’s implementation in UE will be covered in subsequent articles. The next article will discuss the C++ features that UE’s reflection relies upon; previously published articles can be found here: Analysis of UE4 Reflection Implementation: C++ Features.
References: