UE reflection impl analysis: basic concepts

UE反射实现分析:基础概念

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:

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
2
3
4
5
6
class ClassRef
{
public:
int32 ival = 666;
bool func(int32 InIval){ return false;}
};

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once
#include "CoreMinimal.h"
#include "RefObject.generated.h"

UCLASS()
class REF_API URefObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
int32 ival = 666;

UFUNCTION()
bool func(int32 InIval)
{
UE_LOG(LogTemp,Log,TEXT("Function func: %d"),InIval);
return true;
}
};

Key points to note include:

  1. The RefObject.generated.h file
  2. The UCLASS marker
  3. The GENERATED_BODY marker
  4. The UPROPERTY marker
  5. 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
2
3
4
5
6
7
8
9
10
11
12
13
URefObject::URefObject(const FObjectInitializer& Initializer):Super(Initializer)
{
for(TFieldIterator<FProperty> PropertyIter(GetClass());PropertyIter;++PropertyIter)
{
FProperty* PropertyIns = *PropertyIter;
UE_LOG(LogTemp,Log,TEXT("Property Name: %s"),*PropertyIns->GetName());
}
for(TFieldIterator<UFunction> PropertyIter(GetClass());PropertyIter;++PropertyIter)
{
UFunction* PropertyIns = *PropertyIter;
UE_LOG(LogTemp,Log,TEXT("Function Name: %s"),*PropertyIns->GetName());
}
}

Execution result:

1
2
3
LogTemp: Property Name: ival
LogTemp: Function Name: func
LogTemp: Function Name: ExecuteUbergraph

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
2
3
4
5
6
7
8
9
for(TFieldIterator<FProperty> PropertyIter(GetClass());PropertyIter;++PropertyIter)
{
FProperty* PropertyIns = *PropertyIter;
if(PropertyIns->GetName().Equals(TEXT("ival")))
{
int32* i32 = PropertyIns->ContainerPtrToValuePtr<int32>(this);
UE_LOG(LogTemp,Log,TEXT("Property %s value is %d"),*PropertyIns->GetName(),*i32);
}
}

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
2
3
4
5
6
// defined in UObject
virtual void ProcessEvent
(
UFunction * Function,
void * Parms
)

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
2
3
4
5
6
7
8
DEFINE_FUNCTION(URefObject::execfunc)
{
P_GET_PROPERTY(FIntProperty,Z_Param_InIval);
P_FINISH;
P_NATIVE_BEGIN;
*(bool*)Z_Param__Result=P_THIS->func(Z_Param_InIval);
P_NATIVE_END;
}

After expanding the DEFINE_FUNCTION macro, it reveals a fixed prototype:

1
2
#define DEFINE_FUNCTION(func) void func( UObject* Context, FFrame& Stack, RESULT_DECL )
void URefObject::execfunc( UObject* Context, FFrame& Stack, RESULT_DECL )

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:

  1. ProcessEvent is a member function of UObject
  2. The first parameter of ProcessEvent must be a UFunction within the current class
  3. 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
2
3
4
5
struct RefObject_eventFunc_Parms
{
int32 ival;
bool ReturnValue;
};

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for(TFieldIterator<UFunction> PropertyIter(GetClass());PropertyIter;++PropertyIter)
{
UFunction* FuncIns = *PropertyIter;
if(FuncIns->GetName().Equals(TEXT("func")))
{
struct RefObject_eventFunc_Parms
{
int32 ival;
bool ReturnValue;
}func_params;
func_params.ival = 111;
this->ProcessEvent(FuncIns,&func_params);
UE_LOG(LogTemp,Log,TEXT("call func return: %s"),func_params.ReturnValue?TEXT("true"):TEXT("false"));
}
UE_LOG(LogTemp,Log,TEXT("Function Name: %s"),*FuncIns->GetName());
}

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
2
3
4
UFUNCTION()
int32& Add(int32 R, int32& L);
UFUNCTION()
int32 Add(int32 R, int32 L);

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
2
3
4
5
6
7
8
9
class ClassRef
{
public:
ClassRef(int& InIval):ival(InIval){}
int& ival;
};
// instance
int ival = 666;
ClassRef obj(ival);

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:

  1. Assigning parameters to the function parameter structure of ProcessEvent
  2. 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:

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: basic concepts
Author:LIPENGZHA
Publish Date:2020/12/12 23:56
Word Count:8.1k Words
Link:https://en.imzlp.com/posts/12624/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!