相关文章推荐
精明的小马驹  ·  DatePicker - WPF .NET ...·  3 月前    · 
有腹肌的小熊猫  ·  javascript - ...·  1 年前    · 
潇洒的匕首  ·  Neovim + Lua: how to ...·  1 年前    · 
首发于 InsideUE5
《InsideUE4》UObject(十三)类型系统-反射实战

《InsideUE4》UObject(十三)类型系统-反射实战

爱,代码+喵星人

引言

上篇章节总结了类型系统的最后一些小知识点,为了免于说都是纯理论知识,本篇我们来讲一些利用反射的例子。

获取类型对象

如果想获取到程序里定义的所有的class,方便的方法是:

TArray<UObject*> result;
GetObjectsOfClass(UClass::StaticClass(), result);   //获取所有的class和interface
GetObjectsOfClass(UEnum::StaticClass(), result);   //获取所有的enum
GetObjectsOfClass(UScriptStruct::StaticClass(), result);   //获取所有的struct

GetObjectsOfClass是UE4已经写好的一个很方便的方法,可以获取到属于某个UClass*下面的所有对象。因此如果用UClass::StaticClass()本身,就可以获得程序里定义的所有class。值得注意的是,UE4里的接口是有一个配套的UInterface对象来存储元数据信息,它的类型也是用UClass*表示的,所以也会获得interface。根据前文,enum会生成UEnum,struct会生成UScriptStruct,所以把参数换成UEnum::StaticClass()就可以获得所有的UEnum*对象了,UScriptStruct::StaticClass()就是所有的UScriptStruct*了,最后就可以根据这些类型对象来反射获取类型信息了。

而如果要精确的根据一个名字来查找某个类型对象,就可以用UE4里另一个方法:

template< class T > 
inline T* FindObject( UObject* Outer, const TCHAR* Name, bool ExactClass=false )
    return (T*)StaticFindObject( T::StaticClass(), Outer, Name, ExactClass );
UClass* classObj=FindObject<UClass>(ANY_PACKAGE,"MyClass");   //获得表示MyClass的UClass*

这样就可以轻易的获得不同类型的对象。FindObject内部的原理在下大章节内存管理再讲述。

遍历字段

在获取到了一个类型对象后,就可以用各种方式去遍历查找内部的字段了。为此,UE4提供了一个方便的迭代器TFieldIterator<T>,可以通过它筛选遍历字段。

const UStruct* structClass; //任何复合类型都可以
//遍历属性
for (TFieldIterator<UProperty> i(structClass); i; ++i)
    UProperty* prop=*i; 
//遍历函数
for (TFieldIterator<UFunction> i(structClass); i; ++i)
    UFunction* func=*i; 
    //遍历函数的参数
    for (TFieldIterator<UProperty> i(func); i; ++i)
        UProperty* param=*i; 
        if( param->PropertyFlags & CPF_ReturnParm ) //这是返回值
//遍历接口
const UClass* classObj; //只有UClass才有接口
for (const FImplementedInterface& ii : classObj->Interfaces)
    UClass* interfaceClass = ii.Class;

给模板参数T传UFunction就可以获得类型下的所有函数,通过这也可以遍历获得UFunction下的参数列表。当然TFieldIterator也可以再传其他参数的控制是否包含基类的字段、是否包含废弃的字段、是否包含接口里的字段。TFieldIterator的内部实现其实也挺简单的,一是通过SuperStruct来获得Super,二是通过Interfaces来获得实现的接口,三是用Field->Next来遍历字段。信息数据都是全的,迭代遍历就简单了。

遍历枚举的字段也很简单:

const UEnum* enumClass;
for (int i = 0; i < enumClass->NumEnums(); ++i)
    FName name = enumClass->GetNameByIndex(i);
    int value = enumClass->GetValueByIndex(i);

还有遍历元数据的:

#if WITH_METADATA
const UObject* obj;//可以是任何对象,但一般是UField才有值
UMetaData* metaData = obj->GetOutermost()->GetMetaData();
TMap<FName, FString>* keyValues = metaData->GetMapForObject(obj);
if (keyValues != nullptr&&keyValues->Num() > 0)
    for (const auto& i : *keyValues)
        FName key=i.Key;
        FString value=i.Value;
#endif

当然,如果想精确查找的话,也有相应的方法。

//查找属性
UProperty* UStruct::FindPropertyByName(FName InName) const
    for (UProperty* Property = PropertyLink; Property != NULL; Property = Property->PropertyLinkNext)
        if (Property->GetFName() == InName)
            return Property;
    return NULL;
//查找函数
UFunction* UClass::FindFunctionByName(FName InName, EIncludeSuperFlag::Type IncludeSuper) const;

查看继承

得到类型对象后,也可以遍历查看它的继承关系。 遍历继承链条:

const UStruct* structClass; //结构和类
TArray<FString> classNames;
classNames.Add(structClass->GetName());
UStruct* superClass = structClass->GetSuperStruct();
while (superClass)
    classNames.Add(superClass->GetName());
    superClass = superClass->GetSuperStruct();
FString str= FString::Join(classNames, TEXT("->")); //会输出MyClass->UObject

那反过来,如果想获得一个类下面的所有子类,可以这样:

const UClass* classObj; //结构和类
TArray<UClass*> result;
GetDerivedClasses(classObj, result, false);
//函数原型是
void GetDerivedClasses(UClass* ClassToLookFor, TArray<UClass *>& Results, bool bRecursive);

GetDerivedClasses也是UE4里写好的一个方法,内部用到了HashMa方式(TMap<UClass*, TSet<UClass*>> ClassToChildListMap)保存了类到子类列表的映射。

那么怎么获取实现了某个接口的所有子类呢?呃,没啥好办法,因为可能用的不多,所以没有保存这层映射关系。我们只能暴力的遍历出来:

TArray<UObject*> result;
GetObjectsOfClass(UClass::StaticClass(), result);
TArray<UClass*> classes;
for (UObject* obj : result)
    UClass* classObj = Cast<UClass>(obj);
    if (classObj->ImplementsInterface(interfaceClass))//判断实现了某个接口
        classes.Add(classObj);

获取设置属性值

有了UProperty之后,就可以方便的反射获得其值:

template<typename ValueType>
ValueType* UProperty::ContainerPtrToValuePtr(void* ContainerPtr, int32 ArrayIndex = 0) const
    return (ValueType*)ContainerVoidPtrToValuePtrInternal(ContainerPtr, ArrayIndex);
template<typename ValueType>
ValueType* UProperty::ContainerPtrToValuePtr(UObject* ContainerPtr, int32 ArrayIndex = 0) const
    return (ValueType*)ContainerVoidPtrToValuePtrInternal(ContainerPtr, ArrayIndex);
void* UProperty::ContainerVoidPtrToValuePtrInternal(void* ContainerPtr, int32 ArrayIndex) const
    //check...
    return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
void* UProperty::ContainerUObjectPtrToValuePtrInternal(UObject* ContainerPtr, int32 ArrayIndex) const
    //check...
    return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
//获取对象或结构里的属性值地址,需要自己转换成具体类型
void* propertyValuePtr = property->ContainerPtrToValuePtr<void*>(object);
//包含对象引用的属性可以获得对象
UObject* subObject = objectProperty->GetObjectPropertyValue_InContainer(object);

UE4特意加了void*和UObject*的重载来分别获得结构和对象里的属性值,内部其实有更多的check代码被我忽略掉了。UE4里面把外部的结构或对象值叫做Container(容器),非常合理,包裹着属性的外部的东西不就是容器嘛。另一个可以见到的是属性值的获取其实也非常简明,Offset_Internal就是一开始的时候STRUCT_OFFSET()的时候传进来的属性在结构里内存偏移值。ElementSize是元素内存大小,可以通过ArrayIndex数组索引(比如int values[10]这种固定数组的属性)获取数组里第几号元素值。

也因为获取到的是存放属性值的指针地址,所以其实也就可以*propertyValuePtr=xxx;方便的设置值了。当然如果是从字符串导入设置进去,UE4也提供了两个方法来导出导入:

//导出值
virtual void ExportTextItem( FString& ValueStr, const void* PropertyValue, const void* DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope = NULL ) const; 
FString outPropertyValueString;
property->ExportTextItem(outPropertyValueString, property->ContainerPtrToValuePtr<void*>(object), nullptr, (UObject*)object, PPF_None);
//导入值
const TCHAR* UProperty::ImportText( const TCHAR* Buffer, void* Data, int32 PortFlags, UObject* OwnerObject, FOutputDevice* ErrorText = (FOutputDevice*)GWarn ) const;
FString valueStr;
prop->ImportText(*valueStr, prop->ContainerPtrToValuePtr<void*>(obj), PPF_None, obj);

ExportTextItem和ImportText实际上也是我们在编辑器里在Details面板里选择一个属性进行Ctrl+C Copy和Ctrl+V Paste的时候调用的方法。UE4实际上都是把他们序列化成字符串传递的。

反射调用函数

既然能够得到UFunction了,我们当然也就可以调用他们了。虽然这部分机制是关于蓝图的,但是提前罗列出来也未尝不可。在一个UObject上通过名字调用UFunction方法最简单的方式大概是:

//方法原型
int32 UMyClass::Func(float param1); 
UFUNCTION(BlueprintCallable)
int32 InvokeFunction(UObject* obj, FName functionName,float param1)
    struct MyClass_Func_Parms   //定义一个结构用来包装参数和返回值,就像在gen.cpp里那样
        float param1;
        int32 ReturnValue;
    UFunction* func = obj->FindFunctionChecked(functionName);
    MyClass_Func_Parms params;
    params.param1=param1;
    obj->ProcessEvent(func, &params);
    return params.ReturnValue;
int r=InvokeFunction(obj,"Func",123.f);

特别注意的是,我们需要定义一块内存用来包装存储参数和返回值,就像注册函数的时候那样。在gen.cpp里也是这么一块内存,表示参数的UProperty里的Offset其实就是针对这块内存而说的。所以为了能够正确的根据Offset再反取出来值来,这块内存的字段内存布局必须和函数注册时候的保持严格一致!所以字段声明的顺序是必须和gen.cpp里一致。也就是按照先参数顺序后返回值的顺序。

ProcessEvent也是UE4里事先定义好的非常方便的函数,内部会自动的处理蓝图VM的问题。当然,更底层的方法也可以是:

//调用1
obj->ProcessEvent(func, &params);
//调用2
FFrame frame(nullptr, func, &params, nullptr, func->Children);
obj->CallFunction(frame, &params + func->ReturnValueOffset, func);
//调用3
FFrame frame(nullptr, func, &params, nullptr, func->Children);
func->Invoke(obj, frame, &params + func->ReturnValueOffset);

调用123其实是差不多等价的,在没有obj的情况下调用static函数,可以用调用3的方式。我们知道写在蓝图里的函数和事件最终也都是会编译生成的UFunction对象的,所以用此方法可以直接调用蓝图里的成员函数和自定义事件。

当然我们也见到上述的方法也有不便之处,必须手动定一个参数结构和固定的函数原型。这是我自己写的一个通过反射调用函数的方法:

template<typename... TReturns, typename... TArgs>
void InvokeFunction(UClass* objClass, UObject* obj, UFunction* func, TTuple<TReturns...>& outParams, TArgs&&... args)
    objClass = obj != nullptr ? obj->GetClass() : objClass;
    UObject* context = obj != nullptr ? obj : objClass;
    uint8* outPramsBuffer = (uint8*)&outParams;
    if (func->HasAnyFunctionFlags(FUNC_Native)) //quick path for c++ functions
        TTuple<TArgs..., TReturns...> params(Forward<TArgs>(args)..., TReturns()...);
        context->ProcessEvent(func, &params);
        //copy back out params
        for (TFieldIterator<UProperty> i(func); i; ++i)
            UProperty* prop = *i;
            if (prop->PropertyFlags & CPF_OutParm)
                void* propBuffer = prop->ContainerPtrToValuePtr<void*>(&params);
                prop->CopyCompleteValue(outPramsBuffer, propBuffer);
                outPramsBuffer += prop->GetSize();
        return;
    TTuple<TArgs...> inParams(Forward<TArgs>(args)...);
    void* funcPramsBuffer = (uint8*)FMemory_Alloca(func->ParmsSize);
    uint8* inPramsBuffer = (uint8*)&inParams;
    for (TFieldIterator<UProperty> i(func); i; ++i)
        UProperty* prop = *i;
        if (prop->GetFName().ToString().StartsWith("__"))
            //ignore private param like __WolrdContext of function in blueprint funcion library
            continue;
        void* propBuffer = prop->ContainerPtrToValuePtr<void*>(funcPramsBuffer);
        if (prop->PropertyFlags & CPF_OutParm)
            prop->CopyCompleteValue(propBuffer, outPramsBuffer);
            outPramsBuffer += prop->GetSize();
        else if (prop->PropertyFlags & CPF_Parm)
            prop->CopyCompleteValue(propBuffer, inPramsBuffer);
            inPramsBuffer += prop->GetSize();
    context->ProcessEvent(func, funcPramsBuffer);   //call function
    outPramsBuffer = (uint8*)&outParams;    //reset to begin
    //copy back out params
    for (TFieldIterator<UProperty> i(func); i; ++i)
        UProperty* prop = *i;
        if (prop->PropertyFlags & CPF_OutParm)
            void* propBuffer = prop->ContainerPtrToValuePtr<void*>(funcPramsBuffer);
            prop->CopyCompleteValue(outPramsBuffer, propBuffer);
            outPramsBuffer += prop->GetSize();

哇,这么复杂!不是我坏,而是这个函数处理了各种情况。

  1. 利用了UE4本身内部的模板类TTuple来自动的包装多个参数,这样就不用手动的去定义参数结构体了。多返回值也是用TTuple来返回的。
  2. 可以调用C++里定义的成员函数和静态函数,同时也支持调用定义在蓝图里的成员函数,事件,蓝图库里的函数。因为蓝图里的函数支持多输入多输出值,蓝图函数在编译之后,参数常常就会比C++里看起来的多一些,函数中的临时变量也常常被当做一个UProperty了。所以如何压参到蓝图VM就会比C++里多一些额外的步骤。我在上面的函数里面在栈上面生成了一块 FMemory_Alloca(func->ParmsSize); 内存来当做函数运行时候的参数内存。当然,这块内存在反射调用前必须从函数参数集合里初始化,反射调用后又必须把值拷贝回返回参数上。
  3. 在调用静态函数的时候,因为其实不需要UObject*,所以可以传nullptr,于是就可以把相应的UClass*来当做context调用对象了。
  4. 这个函数处理的最复杂的情况,对于无参数和无返回值的情况,读者朋友们可以自己用不定参数模板偏特化和重载来定义出不同的版本来。也是比较容易的。
  5. 我们也看到函数参数里需要你提供UClass*、UObject*和UFunction*,这些参数利用上面的FindFunctionByName类似方式来查找。我们也可以在这上面继续加上一些便利方法来暴露给蓝图。

假如你还想就像在蓝图中调用蓝图函数库一样,只提供函数名字和参数就可以调用,你可以这样:

template<typename... TReturns, typename... TArgs>
static void InvokeFunctionByName(FName functionName,TTuple<TReturns...>& outParams,TArgs&&... args)
    错误!在PIE模式下,有可能会获得SKEL_XXX_C:Func这个对象,里面的Script为空