欢迎光临散文网 会员登陆 & 注册

【UE4】利用反射调用UFunction

2022-07-07 16:00 作者:天空游荡的鱼  | 我要投稿

    前面简单分析了元组(TTuple)是如何展开的,因为在UFunction的反射调用中会用到元组。在本文中,将会详细讲讲反射的方式如何调用UFunction。

    假设有这样的一种场景,需要编写一个通用的函数调用框架。传入函数名或者UFunction和对应参数就可以调用到该UFunction,而不论它是C++中的函数还是蓝图中的事件或函数。并且框架并不知道对应函数的参数有多少个,每个参数类型是什么。那么,就可能会用到本文中讲到的反射调用了。

函数调用

    这是调用框架函数的部分,通用的函数为CallFunction2, 第一个参数是函数名(或者蓝图的事件名)。 TTuple存放的是函数的返回值。并且是支持多返回值的。通过TTuple的Get<index>方法获取对应的返回值。

    通过for (TFieldIterator<FProperty> ParamIt(Function); ParamIt; ++ParamIt)迭代器,可以访问UFunction的所有属性,包括参数,返回值。他们都以FProperty表示。FProperty.PropertyFlags标识了该属性是什么类型的。例如:CPF_OutParm表示它为返回参数(返回值和非const的引用都是返回参数)。CPF_ReturnParm特指返回值。CPF_Parm表示该属性为参数。

    同理,UFunction也有自己的标识在Script.h的EFunctionFlags中定义。本文中只要用FUNC_Native检查该Function是否C++实现。

反射调用函数CallFunction2实现

    这是一个模板函数,定义类返回值和参数类型(TReturn, TArgs)。参数包含被调用函数名FunctionName, TTuple<TReturn...>&元组类型的返回参数,不定长参数模板TArgs&& ...

    内部代码是通过FindObject查找对应的UFunction实例,最后调用CallInternal2,该函数才是真正实现反射调用的地方。

反射实现细节

    函数定义与上面几乎一致,多一个UClass*表示该函数属于哪个对象。

    反射调用分为两种方法

  1. 通过Invoke调用

  2. 通过ProcessEvent

    Invoke是更底层的方法,ProcessEvent内部也会调用Invoke,后面会把两种调用的完成代码都贴上。

    接下来就正式进入反射的详细流程啦。

  • 准备参数

第一行,直接将参数的TTuple参数取地址,等到返回参数的首地址

第二行,将不定长的调用参数封装到TTuple中

第三行,获得TTuple不定长参数的首地址,函数要的参数都在这里啦

第四行,分配一块UFunction参数一样的大的内存,用于存放调用参数,参数总大小可以通过Function.ParamSize获得

第五行,确定该函数是否有返回值

第六行,根据返回值的位置确定入参总大小

第七行,将TTuple入参中的值Copy到新分配的参数内存块中

这些操作都是在为调用Invoke或ProcessEvent做准备,如果是调用Invoke还需要准备调用Frame。下面我们就来看看。

  • 如果是NativeFunc

    继续准备参数

第一行,构建一个调用Frame,第二个参数为要调用的UFunction,第三个参数为入参(在前面已经准备好了)

第二行,取出Frame.OutParams的地址,这里将向Frame的OutParam中放入非const类的引用参数。

    for循环中迭代UFunction的所有属性(包含参数和返回值),如果参数为CPF_OutParm类型且不为CPF_ReturnParm就将入参中对应的值复制到FuncParamsStructAddr中。在这里为每个需要的属性分配了FOutParmRec,从而去构建Frame.OutParam的属性链表。

    通过Property->ContainerPtrToValuePtr<void*>(FuncParamsStructAddr)可以得到该属性的指针偏移,从而把入参中非const 引用参数复制到参数内存。FProperty有Property->GetOffset_ForInternal(), Property->GetSize()等方法可以确定对应参数内存大小。

    参数准备完成之后,开始调用方法并处理返回值。

    Invoke第一个参数为Function所在的UClass,第二个参数为上面构建的Frame,第三个参数为接收返回值的内存地址。在本代码中,统一用参数内存的返回值部分进行接收。ReturnValueAddress是内存参数偏移到返回值部分的首地址。

  • 如果不是NativeFunc

    将调用ProcessEvent进行函数调用。准备参数阶段是一样的。

  • 处理返回值

    第一行,取调用者传入的返回值地址(TTuple)

    第二行,遍历Function的属性

    第四行,判断是否为CPF_OutParm

    第五行,根据属性的地址偏移,取到参数内存中,该参数对应的内存地址

    第六行,将该内存地址的值复制到返回参数中

    第七行,返回参数地址偏移属性大小

    循环完成之后,已经将所有的返回值复制到调用者传入的TTuple中。

问题:

  • 实测中发现,如果有非const 类型的FString引用,当处理函数中修改了引用值,可能会导致异常。

  • 注意,调用时,最好加上Forward,否则会出问题。当被调用的参数为引用类型时,TTuple展开之后也是引用,将会把数值的内存地址传给执行函数而不是值本身。例如:

    上面定义的函数,npcType和OutType为引用类型。通过下面的代码去调用,会得到正确的结果,如果不加Forward,传入参数就不为101和202

    

下面是两种调用的完整代码

    


【UE4】利用反射调用UFunction的评论 (共 条)

分享到微博请遵守国家法律