【UE4】利用反射调用UFunction
前面简单分析了元组(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*表示该函数属于哪个对象。
反射调用分为两种方法
通过Invoke调用
通过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

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