反射的定义是能让程序在运行时知道类型信息,而不限于编译期,具体实现方法牵扯的内容很多,大钊的专栏有一系列文章专门讲虚幻是如何生成和维护各个类型的反射代码的,我这里并不打算将这些过程都梳理一遍,而是想从日常开发中会遇到的一些问题入手,看看虚幻是怎么解决这些问题的。
UClass是怎么表现类型信息的?
StaticClass
获取当前UObject类对应的UClass
,每个UObject都有,查看.generated.h
文件可以找到是在DECLARE_CLASS
宏中定义的,实现是调用了全局方法GetPrivateStaticClassBody
,先通过FindObject
看看能不能用提供的outer和name找到对应的UClass
,如果找不到则new一个新的出来并初始化。
UClass自己调用StaticClass会返回什么?大钊的文章提到会引用到自身,这点还需要看下代码求证。
在引擎启动开始初始化时,会为每个UObject子类调用StaticClass
创建UClass
UPROPERTY宏的作用
被这个宏标记的成员会在.gen.cpp
中生成NewProp_
开头的反射数据(包括名字、类型flag、地址偏移等),并被加入到PropPointers
数组中,ConstructFProperties
中会使用这些数据构造出对应类型的FProperty
如果是UObject成员变量会被当做被这个类引用?在gc时不会被删除
如果在外部被删除则会自动置空
如何通过名字拿到类中的值
直接看一种可行的方式:
void GetPropertyValueByName(UObject* Object, const FString& PropertyName)
{
// Ensure the object is valid
if (!Object) return;
// Get the UClass object of the UObject instance
UClass* ObjectClass = Object->GetClass();
// Find the property by name
FProperty* Property = ObjectClass->FindPropertyByName(*PropertyName);
if (Property)
{
// Found the property
if (FIntProperty* IntProperty = CastField<FIntProperty>(Property))
{
int32 PropertyValue = IntProperty->GetPropertyValue(Property->ContainerPtrToValuePtr<void>(Object));
UE_LOG(LogTemp, Log, TEXT("Property '%s' value: %d"), *PropertyName, PropertyValue);
}
else if (FFloatProperty* FloatProperty = CastField<FFloatProperty>(Property))
{
float PropertyValue = FloatProperty->GetPropertyValue(Property->ContainerPtrToValuePtr<void>(Object));
UE_LOG(LogTemp, Log, TEXT("Property '%s' value: %f"), *PropertyName, PropertyValue);
}
}
}
上面的代码做了以下几件事:
- 找到对应的
UClass
,调用FindPropertyByName
通过名字拿到对应属性FProperty
- 通过
ContainerPtrToValuePtr
从属性中拿到对应实例的 - 确定属性的类型,然后调用
FProperty
中的GetPropertyValue
,使用第二步得到的地址作为参数得到值
为了搞清楚上面发生的事情,首先要理解虚幻是如何处理反射数据的,4.25版本引入了FProperty
代替UProperty
类型,它继承自FFiled
,不继承自UObject,让属性的保存更加轻量化,内存管理上也可以和UObject脱钩。属性通过链表保存在UStruct
(UClass
的基类)中,有一个ChildProperties
成员作为表头,不过FindPropertyByName
是通过另一个成员PropertyLink
进行遍历查找,原理是一样的。
在生成反射数据代码时,有一个偏移值,通过STRUCT_OFFSET
获取,它的实现如下:
#define offsetof(s,m) ((::size_t)&reinterpret_cast<char const volatile&>((((s*)0)->m)))
其中s
是类,m
是成员,这里用0
地址转换成s
类型的指针后,对m
进行访问,然后将其转化为一个字节的char
并取地址,得到的结果便是这个成员相对于所在类的偏移量。
第二步通过“容器”,也就是真正包含这个属性的对象的地址,和FProperty
中储存的地址偏移成员Offset_Internal
,便可以得到对应成员的地址,如果是数组,就再根据给定的下标和ElementSize
进一步偏移。最后调用属性子类中的GetPropertyValue
方法,将地址转换成所需要的指针,之所以不直接使用,一是为了避免用户进行指针类型的转换,二是可以在不同的属性子类中做一些特殊处理。
如何去遍历所有的成员变量
使用TFieldIterator
,它会通过传入的是UField
还是FField
,调用不同特化版本的GetChildFieldsFromStruct
模板函数,从UStruct
中获取Children
或ChildProperties
进行遍历。
如何通过名字调用函数
在UHT收集反射信息时,会对每个被UFUNCTION
标记的函数根据其参数、返回值等信息生成一个调用ConstructUFunction
的函数(名字是根据类型和函数名拼接而成的,暂且叫它FuncA
),然后和被标记函数的名字进行绑定,当ConstructUClass
调用时,作为类中的一部分信息被传入,处理函数信息时便调用前面生成的FuncA
生成UFunction
实例,最后用函数的名字作为key放在UClass的FuncMap
成员中。最后处理绑定和链接信息。
exec开头函数的作用
UHT为每个不是BlueprintImplementableEvent
的UFUNCTION
生成一个带exec
前缀的静态函数,使用的宏定义如下:
// Blueprint VM intrinsic return value declaration.
#define RESULT_PARAM Z_Param__Result
#define RESULT_DECL void*const RESULT_PARAM
// 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 )
它的实现是从字节码中拿到该函数的参数,执行真正的实现,这为蓝图调用C++代码打好了基础。
IMPLEMENT_CLASS
会使用名为StaticRegisterNatives##TClass
的方法来注册当前类中所有Native函数(在c++中有实现的函数),这个函数会在gen.cpp
中生成。
UClass在第一次调用GetPrivateStaticClassBody
时会执行注册,注册的对象便是这些exec开头的函数,它们会按名字和地址被注册到UClass的NativeFunctionLookupTable
成员中去。
在UFunction执行绑定时(UFunction::Bind
),有两种情况:
- 是native函数:会从lookup表中找到与函数名字对应的函数指针,赋值给成员
Func
- 不是native函数:将
UObject::ProcessInternal
赋值给Func
这种类型签名的函数引擎中有一个专门的类型定义,被用在各种反射函数调用上,上面提到的Func
就是这个类型:
/** The type of a native function callable by script */
typedef void (*FNativeFuncPtr)(UObject* Context, FFrame& TheStack, RESULT_DECL);
ProcessEvent
参数为一个UFunction和一个void
类型的指针用来传递参数结构。
它会根据UFunction上面的FunctionFlags判断是否是RPC函数,如果是则调用CallRemoteFunction
。
如果需要本地调用,则创建一个FFrame
,使用UFunction中的Script
成员初始化字节码Code
(UFunction继承的是UStruct,其中包含了字节码信息),然后通过Step
解析字节码。
最后会通过UFunction调用Invoke
,最终调用到Func
。
FFrame的作用
参考:南京周润发关于蓝图虚拟机的文章
这个其实是函数调用时栈帧的概念,可以理解为这次函数调用所需的上下文等信息,FFrame
包含编译后函数对应的字节码,每次UFunction
调用都会创建一个,在蓝图虚拟机中使用。
其成员函数Step
通过字节码不停从GNatives
数组(类型为FNativeFuncPtr
)中拿到对应的函数进行调用,直到字节码的类型为EX_Return
,即函数返回了,也就是说字节码中枚举了函数调用中的各种情况,在蓝图编译时按语句生成字节码供虚拟机执行。
C++如何调用蓝图实现的函数
被标记为可以被蓝图实现的函数,UHT会在gen.cpp
中生成一份实现,其中调用的是ProcessEvent
,下面是UserWidget.gen.cpp
的两个例子:
struct UserWidget_eventOnFocusReceived_Parms
{
FGeometry MyGeometry;
FFocusEvent InFocusEvent;
FEventReply ReturnValue;
};
static FName NAME_UUserWidget_OnFocusReceived = FName(TEXT("OnFocusReceived"));
FEventReply UUserWidget::OnFocusReceived(FGeometry MyGeometry, FFocusEvent InFocusEvent)
{
UserWidget_eventOnFocusReceived_Parms Parms;
Parms.MyGeometry=MyGeometry;
Parms.InFocusEvent=InFocusEvent;
ProcessEvent(FindFunctionChecked(NAME_UUserWidget_OnFocusReceived),&Parms);
return Parms.ReturnValue;
}
static FName NAME_UUserWidget_OnInitialized = FName(TEXT("OnInitialized"));
void UUserWidget::OnInitialized()
{
ProcessEvent(FindFunctionChecked(NAME_UUserWidget_OnInitialized),NULL);
}
可以看到除了UFunction
指针之外,参数通过预先定义好的结构体(安排的明明白白)作为指针传入,如果没有参数则直接传入NULL
,这里不需要知道类型,因为它在作为函数的一部分信息调用ConstructUFunction
的时候,已经按照其结构体大小分配了空间。
蓝图如何调用C++实现的函数
通过生成的字节码调用exec函数,其通过解析字节码的数据,将参数还原出来,然后调用C++的实现。