属性反射实现分析

当在 UCLASS 类中给一个属性添加 UPROPERTY 标记时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UCLASS()
class MICROEND_423_API AMyActor : public AActor
{
GENERATED_BODY()
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;

public:
// Called every frame
virtual void Tick(float DeltaTime) override;

UPROPERTY(EditAnywhere)
int32 ival;
};

会生成反射代码,生成反射代码的代码是在 UHT 中的Programs/UnrealHeaderTool/Private/CodeGenerator.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData[] = {
{ "Category", "MyActor" },
{ "ModuleRelativePath", "Public/MyActor.h" },
};
#endif
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival = {
"ival",
nullptr,
(EPropertyFlags) 0x0010000000000001,
UE4CodeGen_Private::EPropertyGenFlags::Int,
RF_Public | RF_Transient | RF_MarkAsNative,
1,
STRUCT_OFFSET(AMyActor, ival),
METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData))
};

首先 const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival 是创建出一个结构,用于记录当前属性的信息,比如变量名、相对于对象起始地址的偏移。

注意 UE4CodeGen_Private::FIntPropertyParams 这样的类型都是定义在 UObject/UObjectGlobals.h 中的,对于基础类型的属性(如 int8/int32/float/array/map)等使用的都是FGenericPropertyParams

F*PropertyParams

UObject/UObjectGlobals.h 文件中,引擎里定义了很多的F*PropertyParams,但是他们的数据结构基本相同(但是他们并没有继承关系),都是 POD 的类型,每个数据依次排列,而且不同类型的 Params 尽量都保持了一致的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// UObject/UObjectGlobals.h
struct FGenericPropertyParams // : FPropertyParamsBaseWithOffset
{
const char* NameUTF8;
const char* RepNotifyFuncUTF8;
EPropertyFlags PropertyFlags;
EPropertyGenFlags Flags;
EObjectFlags ObjectFlags;
int32 ArrayDim;
int32 Offset;
#if WITH_METADATA
const FMetaDataPairParam* Z_Construct_UClass_;
int32 NumMetaData;
#endif
};

一个一个来分析它的参数。

NameUTF8

NameUTF8 是属性的 UTF8 的名字,在运行时可以通过 UPropertyGetNameCPP来获取。

RepNotifyFuncUTF8

RepNotifyFuncUTF8 是在当前属性绑定的修改之后的函数名字。

如果属性是这样声明:

1
2
3
4
UPROPERTY(EditAnywhere,ReplicatedUsing=OnRep_Replicatedival)
int32 ival;
UFUNCTION()
virtual void OnRep_Replicatedival() {}

这样生成的反射代码就为:

1
2
3
4
5
6
7
8
9
10
const UE4CodeGen_Private::FIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_ival = { 
"ival",
"OnRep_Replicatedival",
(EPropertyFlags)0x0010000100000021,
UE4CodeGen_Private::EPropertyGenFlags::Int,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(AMyActor, ival),
METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData,ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData))
};

因为 OnRep_Replicatedival 也是 UFUNCTION 的函数,所以可以通过反射来访问。

PropertyFlags

PropertyFlags是一个类型为 EPropertyFlags 的枚举,枚举值按照位排列,根据在 UPROPERTY 中的标记来按照位或来记录当前属性包含的标记信息。
UnrealHeaderTool/Private/CodeGenerator.cpp 中生成代码时会根据当前属性的 flag 和 CPF_ComputedFlags 做位运算:

1
2
3
4
5
6
7
8
9
10
/*
// All the properties that should never be loaded or saved
#define CPF_ComputedFlags (CPF_IsPlainOldData | CPF_NoDestructor | CPF_ZeroConstructor | CPF_HasGetValueTypeHash)

0x0000000040000000 CPF_IsPlainOldData
0x0000001000000000 CPF_NoDestructor
0x0000000000000200 CPF_ZeroConstructor
0x0008000000000000 CPF_HasGetValueTypeHash
*/
EPropertyFlags PropFlags = Prop->PropertyFlags & ~CPF_ComputedFlags;

在运行时可以通过 UPropertyHasAnyPropertyFlags函数来检测是否具有特定的 flag。

一般情况下,可以通过 UProperty 来获取到该参数的标记属性,比如当通过 UFunction 获取函数的参数时,可以区分哪个 UProperty 是输入参数、哪个是返回值。

Flags

第四个参数 Flags 是类型为 UE4CodeGen_Private::EPropertyGenFlags 是一个枚举,定义在 UObject/UObjectGlobals.h 中,标记了当前 Property 的类型。

ObjectFlags

ObjectFlags 是 EObjectFlags 类型的枚举,其枚举值也是按照位来划分的。定义在CoreUObject/Public/UObject/ObjectMacros.h

UHT 生成这部分代码在 Programs/UnrealHeaderTool/Private/CodeGenerator.cpp 中:

1
const TCHAR*   FPropertyObjectFlags = FClass::IsOwnedByDynamicType(Prop) ? TEXT("RF_Public|RF_Transient") : TEXT("RF_Public|RF_Transient|RF_MarkAsNative");

通过 FClass::IsOwnedByDynamicType 函数来检测是否为 PROPERTY 添加 RF_MarkAsNative 的 flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Programs/UnrealHeaderTool/Public/ParserClass.h

/** Helper function that checks if the field is a dynamic type (can be constructed post-startup) */
template <typename T>
static bool IsDynamic(const T* Field)
{
return Field->HasMetaData(NAME_ReplaceConverted);
}

// Programs/UnrealHeaderTool/Private/ParserClass.cpp
bool FClass::IsOwnedByDynamicType(const UField* Field)
{
for (const UField* OuterField = Cast<const UField>(Field->GetOuter()); OuterField; OuterField = Cast<const UField>(OuterField->GetOuter()))
{
if (IsDynamic(OuterField))
{
return true;
}
}
return false;
}

bool FClass::IsOwnedByDynamicType(const FField* Field)
{
for (FFieldVariant Owner = Field->GetOwnerVariant(); Owner.IsValid(); Owner = Owner.GetOwnerVariant())
{
if (Owner.IsUObject())
{
return IsOwnedByDynamicType(Cast<const UField>(Owner.ToUObject()));
}
else if (IsDynamic(Owner.ToField()))
{
return true;
}
}
return false;
}

通过调用 UFieldHasMetaData检测是否具有 TEXT("ReplaceConverted") 的元数据(该元数据就是后面要讲到的 Metadata)。

ArrayDim

ArrayDim 用于记录当前属性的元素数量,当只是声明一个单个对象时,如:

1
2
3
4
5
6
7
8
9
10
11
12
UPROPERTY()
float fval;
UPROPERTY(EditAnywhere)
FString StrVal = TEXT("123456");
UPROPERTY(EditAnywhere)
TSubclassOf<UObject> ClassVal;
UPROPERTY(EditAnywhere)
UTexture2D* Texture2D;
UPROPERTY()
FResultDyDlg ResultDlg;
UPROPERTY()
UMySceneComponent* SceneComp;

这些属性所有的 ArrayDim 均为 1.

但是当使用 C++ 原生数组时:

1
2
UPROPERTY()
int32 iArray[12];

它的 ArrayDim 就为:

1
CPP_ARRAY_DIM(iArray, AMyActor)

CPP_ARRAY_DIM这个宏定义在CoreUObject/Public/UObject/UnrealType.h

1
2
3
/** helper to calculate an array's dimensions **/
#define CPP_ARRAY_DIM(ArrayName, ClassName) \
(sizeof(((ClassName*)0)->ArrayName) / sizeof(((ClassName*)0)->ArrayName[0]))

就是用来计算数组内的元素数量的,普通的非数组属性其值为 1,可以当作是元素数量为 1 的数组。

Offset

STRUCT_OFFSET宏的作用为得到数据成员相对于类起始地址的偏移,通过获取 数据成员指针 得到,类型为size_t

然后当前类中的反射属性都会被添加到 PropPointers 中,也是 UHT 生成的代码:

1
2
3
const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UClass_AMyActor_Statics::PropPointers[] = {
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UClass_AMyActor_Statics::NewProp_ival,
};

在运行时可以通过 UProperty 得到指定的对象值:

1
2
3
4
5
6
7
8
9
for (TFieldIterator<UProperty> It(InActor->GetClass()); It; ++It)
{
UProperty* Property = *It;
if (Property->GetNameCPP() == FString("ival"))
{
int32* i32 = Property->ContainerPtrToValuePtr<int32>(InActor);
UE_LOG(LogTemp, Log, TEXT("Property:%s value:%d"), *Property->GetNameCPP(),i32);
}
}

其中 UProperty 中的 ContainerPtrToValuePtr 系列函数都会转发到ContainerVoidPtrToValuePtrInternal

1
2
3
4
5
6
7
8
9
10
11
FORCEINLINE void* ContainerVoidPtrToValuePtrInternal(void* ContainerPtr, int32 ArrayIndex) const
{
check(ArrayIndex < ArrayDim);
check(ContainerPtr);
if (0)
{
// in the future, these checks will be tested if the property is NOT relative to a UClass
check(!Cast<UClass>(GetOuter())); // Check we are _not_ calling this on a direct child property of a UClass, you should pass in a UObject* in that case
}
return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
}

其实就是拿到 UObject 的指针,然后偏移到指定位置(第二个参数用在对象是数组的情况,用来访问指定下标的元素,默认情况下访问下标为 0)。

Z_Construct_UClass_和 NumMetaData

它们的类型分别为 FMetaDataPairParamint32,用来记录当前反射属性的元数据:比如属性的 Category、注释、所属的文件、ToolTip 信息等等,比如在 C++ 函数上添加的注释能够在编辑器蓝图中看到注释的信息,都是靠解析这些元数据来实现的。

1
2
3
4
5
6
7
8
// CoreUObject/Public/UObject/UObjectGlobals.h
#if WITH_METADATA
struct FMetaDataPairParam
{
const char* NameUTF8;
const char* ValueUTF8;
};
#endif

这两个参数通过 METADATA_PARAMS 包裹,用于处理 WITH_MATEDATA 的不同情况:

1
2
3
4
5
6
// METADATA_PARAMS(x, y) expands to x, y, if WITH_METADATA is set, otherwise expands to nothing
#if WITH_METADATA
#define METADATA_PARAMS(x, y) x, y,
#else
#define METADATA_PARAMS(x, y)
#endif

把 UHT 生成的代码宏展开为:

1
2
3
4
5
6
7
8
#if WITH_METADATA
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData[] = {
{ "Category", "MyActor" },
{ "ModuleRelativePath", "Public/MyActor.h" }
};
#endif

METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_ival_MetaData))

就是把 UE4CodeGen_Private::FMetaDataPairParam 这个类型的数组和数组元素个数传递给 F*PropertyParams,实现在WITH_METADATA 的情况下处理是否具有 Metadata 的情况。

运行时的 Property

引擎中通过 UE4CodeGen_Private::ConstructFProperty 来创建出真正 Runtime 使用的UProperty(4.25 之后是FProperty),定义在UObject/UObjectGlobals.cpp

FBoolPropertyParams 特例

当一个反射的数据是 bool 类型时,引擎产生的反射信息中有一个比较有意思的特例,FBoolPropertyParams,可以看一下它的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct FBoolPropertyParams // : FPropertyParamsBase
{
const char* NameUTF8;
const char* RepNotifyFuncUTF8;
EPropertyFlags PropertyFlags;
EPropertyGenFlags Flags;
EObjectFlags ObjectFlags;
int32 ArrayDim;
uint32 ElementSize;
SIZE_T SizeOfOuter;
void (*SetBitFunc)(void* Obj);
#if WITH_METADATA
const FMetaDataPairParam* MetaDataArray;
int32 NumMetaData;
#endif
};

它具有一个 SetBitFunc 的函数指针。看一下生成的反射代码:

1
2
3
4
5
6
7
8
9
10
// source code 
UPROPERTY()
bool bEnabled;

// gen.cpp
void Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_SetBit(void* Obj)
{
((AMyActor*)Obj)->bEnabled = 1;
}
const UE4CodeGen_Private::FBoolPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled = { "bEnabled", nullptr, (EPropertyFlags)0x0010000000000000, UE4CodeGen_Private::EPropertyGenFlags::Bool | UE4CodeGen_Private::EPropertyGenFlags::NativeBool, RF_Public|RF_Transient|RF_MarkAsNative, 1, sizeof(bool), sizeof(AMyActor), &Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_SetBit, METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_bEnabled_MetaData)) };

注意 :其中的关键是,UHT 给该属性生成了一个SetBit 的函数,why?其他类型的属性都可以通过 STRUCT_OFFSET 来获取该成员的类内偏移,为什么 bool 就不行了呢?

这是因为 C++ 有位域 (bit-field) 这个概念,一个 bool 可能只占 1bit,而不是 1byte,但这不是真正的原因。

真正的原因是,C++ 标准规定了不能对位域进行取地址操作!前面已经提到了 STRUCT_OFFSET 实际上是获取到数据成员的指针,得到的是类内偏移,但是因为 C++ 的不能对位域取地址的规定,STRUCT_OFFSET无法用在位域的成员上的。

[IOS/IEC 14882:2014 §9.6]The address-of operator & shall not be applied to a bit-field, so there are no pointers to bit-fields.

那么这又是因为什么呢?因为系统编址的最小单位是字节而不是位,所以没办法取到 1 字节零几位的地址。也就决定了不能对位域的数据成员取地址。

UE 内其实大量用到了 bool 使用位域的方式来声明(如果不使用位域,bool 类型的空间浪费率达到 87.5% :)),所以 UE 就生成了一个函数来为以位域方式声明的成员设置值。

但是!UE 不支持直接对加了 UPROPERTY 的 bool 使用位域:

1
2
UPROPERTY()
bool bEnabled:1;

编译时会有下列错误:

LogCompile: Error: bool bitfields are not supported.

要写成下列方式:

1
2
UPROPERTY()
uint8 bEnabled:1;

使用这种方式和使用 bool bEnabled; 方式生成的反射代码一模一样,所以,UE 之所以会生成一个函数来设置 bool 的值,是因为既要支持原生 bool,也要支持位域。

通过 UProperty 获取值

如果我知道某个类的对象内有一个属性名字,那么怎么能够得到它的值呢?这个可以基于 UE 的属性反射来实现:

首先通过 TFieldIterator 可以遍历该对象的 UProperty:

1
2
3
4
for (TFieldIterator<UProperty> It(InActor->GetClass()); It; ++It)
{
UProperty* Property = *It;
}

然后可以根据得到的 Property 来判读名字:

1
if (Property->GetNameCPP() == FString("ival"))

检测是指定名字的 Property 后可以通过 UProperty 上的 ContainerPtrToValuePtr 函数来获取对象内该属性的指针:

1
int32* i32 = Property->ContainerPtrToValuePtr<int32>(InActor)

前面讲到过,UPropery 里存储 Offdet 值就是当前属性相对于对象起始地址的偏移。而 ContainerPtrToValuePtr 函数所做的就是得到当前对象偏移 Offset 的地址然后做了类型转换。

Property 的 Flag

通过上面的分析,可以看到 UPROPERTY 添加的标记,如 EditAnywhere 等,会给指定的 Property 生成 FLAG 存储在 F*PropertyParams 结构的第三个参数中,是位描述的。

可选值为 EPropertyFlags 枚举值:

Runtime/CoreUObject/Public/UObject/ObjectMacros.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* Flags associated with each property in a class, overriding the
* property's default behavior.
* @warning When adding one here, please update ParsePropertyFlags()
*/
enum EPropertyFlags : uint64
{
CPF_None = 0,

CPF_Edit = 0x0000000000000001, ///< Property is user-settable in the editor.
CPF_ConstParm = 0x0000000000000002, ///< This is a constant function parameter
CPF_BlueprintVisible = 0x0000000000000004, ///< This property can be read by blueprint code
CPF_ExportObject = 0x0000000000000008, ///< Object can be exported with actor.
CPF_BlueprintReadOnly = 0x0000000000000010, ///< This property cannot be modified by blueprint code
CPF_Net = 0x0000000000000020, ///< Property is relevant to network replication.
CPF_EditFixedSize = 0x0000000000000040, ///< Indicates that elements of an array can be modified, but its size cannot be changed.
CPF_Parm = 0x0000000000000080, ///< Function/When call parameter.
CPF_OutParm = 0x0000000000000100, ///< Value is copied out after function call.
CPF_ZeroConstructor = 0x0000000000000200, ///< memset is fine for construction
CPF_ReturnParm = 0x0000000000000400, ///< Return value.
CPF_DisableEditOnTemplate = 0x0000000000000800, ///< Disable editing of this property on an archetype/sub-blueprint
//CPF_ = 0x0000000000001000, ///<
CPF_Transient = 0x0000000000002000, ///< Property is transient: shouldn't be saved or loaded, except for Blueprint CDOs.
CPF_Config = 0x0000000000004000, ///< Property should be loaded/saved as permanent profile.
//CPF_ = 0x0000000000008000, ///<
CPF_DisableEditOnInstance = 0x0000000000010000, ///< Disable editing on an instance of this class
CPF_EditConst = 0x0000000000020000, ///< Property is uneditable in the editor.
CPF_GlobalConfig = 0x0000000000040000, ///< Load config from base class, not subclass.
CPF_InstancedReference = 0x0000000000080000, ///< Property is a component references.
//CPF_ = 0x0000000000100000, ///<
CPF_DuplicateTransient = 0x0000000000200000, ///< Property should always be reset to the default value during any type of duplication (copy/paste, binary duplication, etc.)
CPF_SubobjectReference = 0x0000000000400000, ///< Property contains subobject references (TSubobjectPtr)
//CPF_ = 0x0000000000800000, ///<
CPF_SaveGame = 0x0000000001000000, ///< Property should be serialized for save games, this is only checked for game-specific archives with ArIsSaveGame
CPF_NoClear = 0x0000000002000000, ///< Hide clear (and browse) button.
//CPF_ = 0x0000000004000000, ///<
CPF_ReferenceParm = 0x0000000008000000, ///< Value is passed by reference; CPF_OutParam and CPF_Param should also be set.
CPF_BlueprintAssignable = 0x0000000010000000, ///< MC Delegates only. Property should be exposed for assigning in blueprint code
CPF_Deprecated = 0x0000000020000000, ///< Property is deprecated. Read it from an archive, but don't save it.
CPF_IsPlainOldData = 0x0000000040000000, ///< If this is set, then the property can be memcopied instead of CopyCompleteValue / CopySingleValue
CPF_RepSkip = 0x0000000080000000, ///< Not replicated. For non replicated properties in replicated structs
CPF_RepNotify = 0x0000000100000000, ///< Notify actors when a property is replicated
CPF_Interp = 0x0000000200000000, ///< interpolatable property for use with matinee
CPF_NonTransactional = 0x0000000400000000, ///< Property isn't transacted
CPF_EditorOnly = 0x0000000800000000, ///< Property should only be loaded in the editor
CPF_NoDestructor = 0x0000001000000000, ///< No destructor
//CPF_ = 0x0000002000000000, ///<
CPF_AutoWeak = 0x0000004000000000, ///< Only used for weak pointers, means the export type is autoweak
CPF_ContainsInstancedReference = 0x0000008000000000, ///< Property contains component references.
CPF_AssetRegistrySearchable = 0x0000010000000000, ///< asset instances will add properties with this flag to the asset registry automatically
CPF_SimpleDisplay = 0x0000020000000000, ///< The property is visible by default in the editor details view
CPF_AdvancedDisplay = 0x0000040000000000, ///< The property is advanced and not visible by default in the editor details view
CPF_Protected = 0x0000080000000000, ///< property is protected from the perspective of script
CPF_BlueprintCallable = 0x0000100000000000, ///< MC Delegates only. Property should be exposed for calling in blueprint code
CPF_BlueprintAuthorityOnly = 0x0000200000000000, ///< MC Delegates only. This delegate accepts (only in blueprint) only events with BlueprintAuthorityOnly.
CPF_TextExportTransient = 0x0000400000000000, ///< Property shouldn't be exported to text format (e.g. copy/paste)
CPF_NonPIEDuplicateTransient = 0x0000800000000000, ///< Property should only be copied in PIE
CPF_ExposeOnSpawn = 0x0001000000000000, ///< Property is exposed on spawn
CPF_PersistentInstance = 0x0002000000000000, ///< A object referenced by the property is duplicated like a component. (Each actor should have an own instance.)
CPF_UObjectWrapper = 0x0004000000000000, ///< Property was parsed as a wrapper class like TSubclassOf<T>, FScriptInterface etc., rather than a USomething*
CPF_HasGetValueTypeHash = 0x0008000000000000, ///< This property can generate a meaningful hash value.
CPF_NativeAccessSpecifierPublic = 0x0010000000000000, ///< Public native access specifier
CPF_NativeAccessSpecifierProtected = 0x0020000000000000, ///< Protected native access specifier
CPF_NativeAccessSpecifierPrivate = 0x0040000000000000, ///< Private native access specifier
CPF_SkipSerialization = 0x0080000000000000, ///< Property shouldn't be serialized, can still be exported to text
};