
UE4 C++反射从入门到原理剖解(UE4 4.26)
UE4 C++反射具体应用案例测试反射类ATestActor// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h"#include "GameFramework/Actor.h"#include "TestActor.gene
UE4 C++反射具体应用案例
获取某类的所有对象(加载到内存)
加载到内存指的是包括蓝图资产, 在关卡里等等已经加载到内存的对象,如果从内存卸载掉的不算。
案例一:获取AMyActor的所有对象
//获取对应基类的对象
TArray<UObject*> result;
GetObjectsOfClass(AMyActor::StaticClass(), result);
for(auto& Object : result)
{
UE_LOG(LogTemp, Warning, TEXT("AMyActor=%s"), *Object->GetName());
}
案例二:获取UEnum的所有对象
TArray<UObject*> result1;
GetObjectsOfClass(UEnum::StaticClass(), result1);
for(auto& Object : result1)
{
UE_LOG(LogTemp, Warning, TEXT("UEnum=%s"), *Object->GetName());
}
案例三: 获取所有反射结构体(UScriptStruct)对象
TArray<UObject*> result2;
GetObjectsOfClass(UScriptStruct::StaticClass(), result2);
for(auto& Object : result2)
{
UE_LOG(LogTemp, Warning, TEXT("UScriptStruct=%s"), *Object->GetName());
}
访问UClass/UScriptStruct/UEnum/UFunction
简介
UClass存储了UObject类信息,如类名字, 类有多少个变量, 变量名字, 变量类型等等
UScriptStruct存储了USTRUCT()结构体的信息, 如结构体名字, 结构体有多少个变量, 变量名字, 变量类型等等
UFunction存储了函数的各种信息, 如函数的名字,函数存在多少个参数, 各个参数的类型又是什么等等
下面以UTestObject, FMyStruct, EMyEnum为案例
UENUM()
enum class EMyEnum : uint8
{
Sucess,
failed
};
USTRUCT()
struct FMyStruct
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
int32 a = 3;
UPROPERTY(EditAnywhere)
float b = 2;
UPROPERTY(EditAnywhere)
FString C = "1243";
};
UCLASS(meta = (DisplayName = "MyTestObject"))
class UTestObject : public UObject
{
GENERATED_BODY()
UTestObject();
public:
UPROPERTY(EditAnywhere)
bool boolVarible = false;
UPROPERTY(EditAnywhere)
EMyEnum EnumVarible = EMyEnum::failed;
UPROPERTY(EditAnywhere)
int32 int32Varible = 15;
UPROPERTY(EditAnywhere)
float floatVarible = 3.1314f;
UPROPERTY(EditAnywhere)
FString stringVarile = "TestString";
UPROPERTY(EditAnywhere)
FVector VectorVarible = FVector::UpVector;
UPROPERTY(EditAnywhere)
FSoftObjectPath SoftPathVarible;
UPROPERTY(EditAnywhere)
UStaticMesh* Mesh;
UPROPERTY(EditAnywhere)
FMyStruct MyStruct;
UPROPERTY(EditAnywhere)
TArray<int32> IntArray = {2, 3, 6};
UPROPERTY(EditAnywhere)
TMap<int32, FString> DataMap = {{1, "a"},{2, "b"}};
UFUNCTION(CallInEditor)
void TestA(int32 A);
UFUNCTION(CallInEditor)
float TestB(int32 A, float B, UStaticMesh* InMesh, TArray<int32> Values);
};
访问UClass以及相关Property
访问各种类型的UPROPERTY成员变量
UTestObject* testObject = NewObject<UTestObject>();
testObject->AddToRoot();
UClass* TestObjectClass = testObject->GetClass();
// 或者 UClass* Class = UTestObject::GetClass();
UE_LOG(LogTemp, Warning, TEXT("UTestObject Class=%s"), *TestObjectClass->GetName());
for (TFieldIterator<FProperty> P(TestObjectClass); P; ++P)
{
FProperty* PropertyPtr = *P;
if(!PropertyPtr)
continue;
FFieldClass* PropertyClass = PropertyPtr->GetClass();
//FFieldClass* BoolClass FBoolProperty::StaticClass();
const FString& PropertyName = PropertyPtr->GetName();
const FString& PropertyClassName = PropertyClass->GetName();
// bool 类型
if(PropertyPtr->IsA(FBoolProperty::StaticClass()))
{
FBoolProperty* BoolProperty = CastFieldChecked<FBoolProperty>(PropertyPtr);
if(BoolProperty)
{
bool* ValuePtr = BoolProperty->ContainerPtrToValuePtr<bool>(testObject);
if(ValuePtr)
{
UE_LOG(LogTemp, Warning, TEXT("PropertyType : %s, PropertyName: %s -- Value: %d"), *PropertyClassName, *PropertyName, (*ValuePtr) == true ? 1 : 0);
}
}
}
// UENUM标注的枚举体
else if(PropertyPtr->IsA(FEnumProperty::StaticClass()))
{
FEnumProperty* EnumProperty = CastFieldChecked<FEnumProperty>(PropertyPtr);
if(EnumProperty)
{
uint8* ValuePtr = EnumProperty->ContainerPtrToValuePtr<uint8>(testObject);
if(ValuePtr)
{
UE_LOG(LogTemp, Warning, TEXT("PropertyType : %s, PropertyName: %s -- Value: %d"), *PropertyClassName, *PropertyName, (*ValuePtr));
}
}
}
// 数值类型(int8, int16, int32, int64, byte, uint16, uint32, uint64, float, double)
else if(PropertyPtr->IsA(FNumericProperty::StaticClass()))
{
if(PropertyPtr->IsA(FInt8Property::StaticClass()))
{
}
else if(PropertyPtr->IsA(FInt16Property::StaticClass()))
{
}
else if(PropertyPtr->IsA(FIntProperty::StaticClass()))
{
FIntProperty* IntProperty = CastFieldChecked<FIntProperty>(PropertyPtr);
if(IntProperty)
{
int32* ValuePtr = IntProperty->ContainerPtrToValuePtr<int32>(testObject);
if(ValuePtr)
{
UE_LOG(LogTemp, Warning, TEXT("PropertyType : %s, PropertyName: %s -- Value: %d"), *PropertyClassName, *PropertyName, (*ValuePtr));
}
}
}
else if(PropertyPtr->IsA(FInt64Property::StaticClass()))
{
}
else if(PropertyPtr->IsA(FByteProperty::StaticClass()))
{
}
else if(PropertyPtr->IsA(FUInt16Property::StaticClass()))
{
}
else if(PropertyPtr->IsA(FUInt32Property::StaticClass()))
{
}
else if(PropertyPtr->IsA(FUInt64Property::StaticClass()))
{
}
else if(PropertyPtr->IsA(FFloatProperty::StaticClass()))
{
FFloatProperty* FloatProperty = CastFieldChecked<FFloatProperty>(PropertyPtr);
if(FloatProperty)
{
float* ValuePtr = FloatProperty->ContainerPtrToValuePtr<float>(testObject);
if(ValuePtr)
{
UE_LOG(LogTemp, Warning, TEXT("PropertyType : %s, PropertyName: %s -- Value: %f"), *PropertyClassName, *PropertyName, (*ValuePtr));
}
}
}
else if(PropertyPtr->IsA(FDoubleProperty::StaticClass()))
{
}
}
// FString类型
else if(PropertyPtr->IsA(FStrProperty::StaticClass()))
{
FStrProperty* StringProperty = CastFieldChecked<FStrProperty>(PropertyPtr);
if(StringProperty)
{
FString* ValuePtr = StringProperty->ContainerPtrToValuePtr<FString>(testObject);
if(ValuePtr)
{
UE_LOG(LogTemp, Warning, TEXT("PropertyType : %s, PropertyName: %s -- Value: %s"), *PropertyClassName, *PropertyName, *(*ValuePtr));
}
}
}
// FText类型
else if(PropertyPtr->IsA(FTextProperty::StaticClass()))
{
}
// FName类型
else if(PropertyPtr->IsA(FNameProperty::StaticClass()))
{
}
// USTRUCT标注结构体类型, 比如FVector, FVector2D, FSoftObjectPath, FMyStruct等等
else if(PropertyPtr->IsA(FStructProperty::StaticClass()))
{
FStructProperty* StructProperty = CastFieldChecked<FStructProperty>(PropertyPtr);
if(StructProperty)
{
if(StructProperty->Struct == TBaseStructure<FVector>::Get())
{
FVector* ValuePtr = StructProperty->ContainerPtrToValuePtr<FVector>(testObject);
if(ValuePtr)
{
UE_LOG(LogTemp, Warning, TEXT("PropertyType : %s, PropertyName: %s -- Value: %s"), *PropertyClassName, *PropertyName, *(*ValuePtr).ToString());
}
}
else if(StructProperty->Struct == TBaseStructure<FSoftObjectPath>::Get())
{
FSoftObjectPath* ValuePtr = StructProperty->ContainerPtrToValuePtr<FSoftObjectPath>(testObject);
if(ValuePtr)
{
UE_LOG(LogTemp, Warning, TEXT("PropertyType : %s, PropertyName: %s -- Value: %s"), *PropertyClassName, *PropertyName, *(*ValuePtr).ToString());
}
}
else if(StructProperty->Struct == TBaseStructure<FMyStruct>::Get())
{
}
}
}
// UObject指针类型, 比如UStaticMesh*
else if(PropertyPtr->IsA(FObjectProperty::StaticClass()))
{
FObjectProperty* ObjectProperty = CastFieldChecked<FObjectProperty>(PropertyPtr);
if(ObjectProperty)
{
UObject* Object = ObjectProperty->GetObjectPropertyValue_InContainer(testObject);
if(ObjectProperty->PropertyClass.Get() == UStaticMesh::StaticClass())
{
FString Value = Object ? Object->GetName() : FString("NullObbject");
UE_LOG(LogTemp, Warning, TEXT("PropertyType : %s, PropertyName: %s -- Value: %s"), *PropertyClassName, *PropertyName, *Value);
}
}
}
// Array
else if(PropertyPtr->IsA(FArrayProperty::StaticClass()))
{
FArrayProperty* ArrayProperty = CastFieldChecked<FArrayProperty>(PropertyPtr);
if(ArrayProperty)
{
void* ArrayVariblePtr = ArrayProperty->ContainerPtrToValuePtr<void>(testObject);
// 数组元素属性
FProperty* InnerProperty = ArrayProperty->Inner;
UE_LOG(LogTemp, Warning, TEXT("PropertyType : %s, PropertyName: %s, InnerProperty: %s"), *PropertyClassName, *PropertyName, *InnerProperty->GetClass()->GetName());
for(int32 DimIndex = 0; DimIndex < ArrayProperty->ArrayDim; DimIndex++)
{
FScriptArrayHelper ArrayHelper(ArrayProperty, ArrayVariblePtr);
const int32 NumberOfItems = ArrayHelper.Num();
for(int32 ArrayIndex = 0; ArrayIndex < NumberOfItems; ArrayIndex++)
{
if(InnerProperty->IsA(FIntProperty::StaticClass()))
{
uint8* ElementRawPtr = ArrayHelper.GetRawPtr(ArrayIndex);
int32* ValuePtr = (int32*)(ElementRawPtr);
if(ValuePtr)
{
UE_LOG(LogTemp, Warning, TEXT("PropertyName: %s[%d], --Value: %d"), *PropertyName, ArrayIndex, *ValuePtr);
}
}
}
}
}
}
else if(PropertyPtr->IsA(FMapProperty::StaticClass()))
{
FMapProperty* MapProperty = CastFieldChecked<FMapProperty>(PropertyPtr);
if(MapProperty)
{
void* MapVariblePtr = MapProperty->ContainerPtrToValuePtr<void>(testObject);
FProperty* KeyProperty = MapProperty->KeyProp;
FProperty* ValueProperty = MapProperty->ValueProp;
UE_LOG(LogTemp, Warning, TEXT("PropertyType : %s, PropertyName: %s, KeyProperty: %s, ValueProperty: %s"), *PropertyClassName,
*PropertyName, *KeyProperty->GetClass()->GetName(), *ValueProperty->GetClass()->GetName())
FScriptMapHelper Helper(MapProperty, MapVariblePtr);
const int32 Num = Helper.Num();
for (int32 DynamicIndex = 0; DynamicIndex < Num; ++DynamicIndex)
{
if (Helper.IsValidIndex(DynamicIndex))
{
uint8* KeyElementRawPtr = Helper.GetKeyPtr(DynamicIndex);
int32* KeyPtr = (int32*)(KeyElementRawPtr);
uint8* ValueElementRawPtr = Helper.GetValuePtr(DynamicIndex);
FString* ValuePtr = (FString*)(ValueElementRawPtr);
if(KeyPtr && ValuePtr)
{
UE_LOG(LogTemp, Warning, TEXT("Key : %d, Value: %s"), *KeyPtr, *(*ValuePtr))
}
}
}
}
}
}
访问UScriptStruct以及相关Property
UScriptStruct保持了USTRUCT结构体的信息
遍历成员变量方式基本和UClass一样
UScriptStruct* MyScriptStruct = TBaseStructure<FMyStruct>::Get();
for (TFieldIterator<FProperty> P(MyScriptStruct); P; ++P)
{
//其他和UClass遍历属性一致
}
访问UEnum
UEnum* EnumClass = StaticEnum<EMyEnum>();
int32 NumEnum = EnumClass->NumEnums();
UE_LOG(LogTemp, Warning, TEXT("MyEnumName: %s"), *EnumClass->GetName());
for(int32 Index = 0; Index < NumEnum; Index++)
{
FName EnumName = EnumClass->GetNameByIndex(Index);
UE_LOG(LogTemp, Warning, TEXT("Enum[%d]: %s"), Index, *EnumName.ToString())
}
访问UFunction以及相关Property
在上面的UTestObject定义两个UFUNCTION函数TestA和TestB
获取UObject或者UScriptStruct的UFunction,并获取函数的各种参数(Property/Param)
UTestObject* testObject = NewObject<UTestObject>();
testObject->AddToRoot();
UClass* TestObjectClass = testObject->GetClass();
//遍历一个类/结构体的函数
for (TFieldIterator<UFunction> F(TestObjectClass); F; ++F)
{
UFunction* Fun = *F;
if(!Fun)
continue;
FString PrintText;
FString FuncName = Fun->GetName();
PrintText = FString::Printf(TEXT("FuncName: %s: "), *FuncName);
// 遍历一个函数参数类型
for (TFieldIterator<FProperty> FuncP(Fun); FuncP; ++FuncP)
{
FProperty* Property = *FuncP;
if(!Property)
continue;
FString PropertyName = Property->GetName();
FString PropertyType = Property->GetClass()->GetName();
PrintText = FString::Printf(TEXT("%s \nPropertyName: %s PropertyType:%s"), *PrintText, *PropertyName, *PropertyType);
}
UE_LOG(LogTemp, Error, TEXT("%s"), *PrintText);
}
如果函数存在返回值, 则函数最后一个参数就是返回值.
调用函数(ProcessEvent)
利用相同的Struct内存布局,来传参. 当然网上有更通用的调用方法,我这里只是一个实例。
// 调用UFunction
UFunction* TestAFunction = TestObjectClass->FindFunctionByName("TestB");
if(TestAFunction)
{
struct TempStruct
{
int32 A;
float B;
UStaticMesh* InMesh;
TArray<int32> Values;
float ReturnValue; // last is return value
};
TempStruct Temp{1, 2.564f, nullptr, {100, 124, 5}};
testObject->ProcessEvent(TestAFunction, &Temp);
UE_LOG(LogTemp, Error, TEXT("ReturnValue = %f"), Temp.ReturnValue);
}
UE4 C++反射的实现原理剖解
CPP反射总体流程
说明一下UE4 CPP反射宏(UClass, UPROPERTY, UFUNCTION)是空宏,最后根本没有参与到C++编译当中,具体见ObjectMacros.h
UBT
UBT全称UnrealBuildTool, 用在建立项目配置,如模块依赖,包含文件目录,设置项目优化配置项等,作用类似于premake, cmake这些东西,就我看到的代码而言,UE4 C++ 反射和UBT几乎没什么关系。UnrealBuildTool.exe文件Engine\Source\Programs\UnrealBuildTool\Development
UHT
UHT 全称UnrealHeaderTool, UE4 C++反射依赖于 UHT这个工具。UHT用在创建新文件xxx.generated.h 和 xxx.gen.cpp , 其中generated.h 主要是相关函数的声明, gen.cpp是具体反射数据的指定(类元数据, 属性反射数据,函数反射数据等)
UnrealHeaderTool.exe文件Engine\Binaries\Win64\
UE4 C++反射原理剖解
上面说了C++反射代码的使用以及C++反射流程。下面具体分析反射实现细节。UE4的实现C++反射原理的机制看起来有点复杂,实际很简单,用一句话概括:利用宏标记,用UHT进行字符串分析宏标记来生成对应相应的硬编码CPP代码(xxx.generated.h和xxx.gen.cpp), 在CPP硬编码中手动指定属性反射数据,函数反射数据,类反射数据。
UE4 反射类结构
拿UClass(UStruct)为例子, 这里UField* Children存储了一个类所有反射函数数据的节点,UField*是一个链表结构,一个UField*节点就是一个UFunction
同样的, FField* ChildPerperties存储了一个类所有反射变量数据的节点,FField*是一个链表结构,一个FField*节点就是一个FProperty
宏标记
UE4提供了UPROPERTY来标记属性的反射数据,用UFUNCTION标记函数的反射数据,用UCLASS标记类的反射数据。
下面拿例子 ATestActor来说明UE4 C++反射的实现
(1)UClass存储了继承UObject的类反射信息, 用UCLASS宏来标记
(2)UScriptStruct存储了FXXX的结构体反射信息, USTRUCT宏来标记
(3)UFunction存储了函数的反射信息, 用UFUNCTION宏来标记
(4)FProperty存储属性的反射信息, 用UPROPERTY宏来标记
UClass类的具体机制
UClass的经典引用判断一个对象是不是一个类
从上图UClass直接代表一个类的类数据, 同个类的类对象的UClass都是同一个, 存储了反射属性数据和反射函数数据等等。这里有个疑惑:UClass是怎么构造出来的?ATestActor::StaticClass() 哪里来的?答案很简单,用GENERATED_BODY 将 “TestActor.generated.h” 和 “TestActor.gen.cpp” 解析的代码包含进行UClass的构造。当然具体过程很长,下面我慢慢分析
GENERATED_BODY
首先看看ObjectMacros.h, 里面很多宏用于实现UE4 C++的反射机制
再看 “TestActor.generated.h” 和TestActor.h
可以知道 CURRENT_FILE_ID 为 TestReflect_Source_TestReflect_TestActor_h。而__LINE__就是GENERATED_BODY所在行号12。 所以GENERATED_BODY 相当于TestReflect_Source_TestReflect_TestActor_h_12_GENERATED_BODY。
这个宏其实在 TestActor.generated.h 也定义了,看看相关代码
TestReflect_Source_TestReflect_TestActor_h_12_PRIVATE_PROPERTY_OFFSET,TestReflect_Source_TestReflect_TestActor_h_12_SPARSE_DATA,TestReflect_Source_TestReflect_TestActor_h_12_RPC_WRAPPERS 等都是在TestActor.generated.h 声明的宏,展开来就是一堆代码, 拿TestReflect_Source_TestReflect_TestActor_h_12_RPC_WRAPPER。举个例子
也就是说GENERATED_BODY的本质就是替换宏,把 宏 TestReflect_Source_TestReflect_TestActor_h_12_GENERATED_BODY展开的C++代码放到TestActor类里, 具体展开就是
代码细节很多,很多具体宏没展开,比如 DECLARE_CLASS, DECLARE_SERALIZER.
这里重点讲解的宏是DECLARE_CLASS,UClass*的生成和StaticClass()函数就和DECLARE_CLASS息息相关
可以看到StaticClass()函数是怎么来的
这里GetPrivateStaticClass()怎么来的?在TestActor.gen.cpp的
水落石出, UClass*和StaticClass静态函数是怎么来的已经一目了然!
这个还有个很小的点, 多年前面试官喜欢问的,一个UObject怎么访问其母类的方法属性, 写过UE4 C++代码都知道是Super::XXX, 从上面DECLARE_CLASS 展开看到就是一个宏替换而已
类(UClass)元数据的注册
我们看到了UClass*的创建, 但是UClass的元数据呢?首先元数据本质就是Key-Vakue形式的值,可以和TMap<FString, String> 等同。类元数据在xxx.gen.cpp硬编码进行的,看TestActor.gen.cpp代码。
上面的Z_Construct_UClass_ATestActor_Statics 是个结构体,包含了各种反射数据(类,属性,函数),可以看到都是静态变量, 这样不用实例化结构体就能保存反射数据了。这里有个小细节,WITH_METADATA说明元数据都是只在编辑器下存在。可以看到反射数据就是硬编码指认到静态变量,有个问题怎么把这个数据放到UClass*里,并且是什么时候放入到的?继续往下看,在TestActor.gen.cpp代码底部
把各种静态反射数据(类元数据,属性元数据,函数元数据) 填充到FClassParams ClassParams里
然后用FClassParams的数据对ATestActor的UClass进行赋值, 比如类元数据,属性数据,函数数据等。
有个问题是Z_Construct_UClass_ATestActor函数什么时候执行? 答案在TestActor.gen.cpp代码的最底部
这个本质上是个静态回调函数, 看看下面代码
每当一个模块(dll)加载进来,对应的UObject的UClass注册流程就开始进行了
属性反射
属性反射数据主要包括属性的名字, 属性的指针偏移(UE4的属性访问是通过变量相对于类的偏移量), 属性元数据等。其实属性反射实现的过程和类的元数据很类似, 都在TestActor.gen.cpp文件里硬编码的.
const UE4CodeGen_Private::FFloatPropertyParams Z_Construct_UClass_ATestActor_Statics::NewProp_a = { "a", nullptr, (EPropertyFlags)0x0010000000000001, UE4CodeGen_Private::EPropertyGenFlags::Float, RF_Public|RF_Transient|RF_MarkAsNative, 1, STRUCT_OFFSET(ATestActor, a), METADATA_PARAMS(Z_Construct_UClass_ATestActor_Statics::NewProp_a_MetaData, UE_ARRAY_COUNT(Z_Construct_UClass_ATestActor_Statics::NewProp_a_MetaData)) };
STRUCT_OFFSET(ATestActor, a) 是求变量a在类ATestActor的偏移字节数, 这点比较巧妙,利用偏移量达到访问属性的目的。最终一样PropPointers是放到ClassParams里,最终被注册到UClass*数据了,从上图可以看出有多少个反射属性就对应多少对硬编码的属性数据(属性元数据以及属性基本信息).
函数反射
函数反射和属性反射一样简单,函数反射主要包括函数的名字,函数的访问,函数的元数据等。
这里函数的各种数据和属性一样的硬编码的,在上面宏展开的代码可以看到
DECLARE_FUNCTION(execTestFunc)
进一步看
在TestActor.gen.cpp可以看到
对应的宏不细说,就是执行了TestFunc, 绕了一圈
这里函数指针的注册到UClass在StaticRegisterNativesATestActor中执行的
至于StaticRegisterNativesATestActor是在上面IMPLAMENT_CLASS展开构造函数中进行的。
至于函数的元数据, 和属性元数据构造过程差不多。
函数的元数据最终也到了ClassParams
UE4 C++ 反射 VS RTTR C++ 反射
我之前一篇博客 RTTR反射原理剖解 分析了另一个C++反射框架 RTTR, 总体上是利用模板元编程进行类型萃取,而整个框架是非侵入式,而UE4的C++反射实现大体是预生成代码加上宏代码替换,硬编码来实现C++反射,从实现上我个人觉得RTTR更灵活(非侵入式),也更优雅(模板推导),当然RTTR明显使用比UE4更多的模板代码,可能会造成代码更慢编译速度问题。
资料参考
更多推荐
所有评论(0)