【UE4】利用反射調(diào)用UFunction
????前面簡單分析了元組(TTuple)是如何展開的,因為在UFunction的反射調(diào)用中會用到元組。在本文中,將會詳細講講反射的方式如何調(diào)用UFunction。
????假設(shè)有這樣的一種場景,需要編寫一個通用的函數(shù)調(diào)用框架。傳入函數(shù)名或者UFunction和對應(yīng)參數(shù)就可以調(diào)用到該UFunction,而不論它是C++中的函數(shù)還是藍圖中的事件或函數(shù)。并且框架并不知道對應(yīng)函數(shù)的參數(shù)有多少個,每個參數(shù)類型是什么。那么,就可能會用到本文中講到的反射調(diào)用了。
函數(shù)調(diào)用
????這是調(diào)用框架函數(shù)的部分,通用的函數(shù)為CallFunction2, 第一個參數(shù)是函數(shù)名(或者藍圖的事件名)。 TTuple存放的是函數(shù)的返回值。并且是支持多返回值的。通過TTuple的Get<index>方法獲取對應(yīng)的返回值。
????通過for (TFieldIterator<FProperty> ParamIt(Function); ParamIt; ++ParamIt)迭代器,可以訪問UFunction的所有屬性,包括參數(shù),返回值。他們都以FProperty表示。FProperty.PropertyFlags標(biāo)識了該屬性是什么類型的。例如:CPF_OutParm表示它為返回參數(shù)(返回值和非const的引用都是返回參數(shù))。CPF_ReturnParm特指返回值。CPF_Parm表示該屬性為參數(shù)。
? ? 同理,UFunction也有自己的標(biāo)識在Script.h的EFunctionFlags中定義。本文中只要用FUNC_Native檢查該Function是否C++實現(xiàn)。
反射調(diào)用函數(shù)CallFunction2實現(xiàn)
????這是一個模板函數(shù),定義類返回值和參數(shù)類型(TReturn, TArgs)。參數(shù)包含被調(diào)用函數(shù)名FunctionName, TTuple<TReturn...>&元組類型的返回參數(shù),不定長參數(shù)模板TArgs&& ...
????內(nèi)部代碼是通過FindObject查找對應(yīng)的UFunction實例,最后調(diào)用CallInternal2,該函數(shù)才是真正實現(xiàn)反射調(diào)用的地方。
反射實現(xiàn)細節(jié)
????函數(shù)定義與上面幾乎一致,多一個UClass*表示該函數(shù)屬于哪個對象。
????反射調(diào)用分為兩種方法
通過Invoke調(diào)用
通過ProcessEvent
????Invoke是更底層的方法,ProcessEvent內(nèi)部也會調(diào)用Invoke,后面會把兩種調(diào)用的完成代碼都貼上。
????接下來就正式進入反射的詳細流程啦。
準(zhǔn)備參數(shù)
第一行,直接將參數(shù)的TTuple參數(shù)取地址,等到返回參數(shù)的首地址
第二行,將不定長的調(diào)用參數(shù)封裝到TTuple中
第三行,獲得TTuple不定長參數(shù)的首地址,函數(shù)要的參數(shù)都在這里啦
第四行,分配一塊UFunction參數(shù)一樣的大的內(nèi)存,用于存放調(diào)用參數(shù),參數(shù)總大小可以通過Function.ParamSize獲得
第五行,確定該函數(shù)是否有返回值
第六行,根據(jù)返回值的位置確定入?yún)⒖偞笮?/p>
第七行,將TTuple入?yún)⒅械闹礐opy到新分配的參數(shù)內(nèi)存塊中
這些操作都是在為調(diào)用Invoke或ProcessEvent做準(zhǔn)備,如果是調(diào)用Invoke還需要準(zhǔn)備調(diào)用Frame。下面我們就來看看。
如果是NativeFunc
繼續(xù)準(zhǔn)備參數(shù)
第一行,構(gòu)建一個調(diào)用Frame,第二個參數(shù)為要調(diào)用的UFunction,第三個參數(shù)為入?yún)?在前面已經(jīng)準(zhǔn)備好了)
第二行,取出Frame.OutParams的地址,這里將向Frame的OutParam中放入非const類的引用參數(shù)。
????for循環(huán)中迭代UFunction的所有屬性(包含參數(shù)和返回值),如果參數(shù)為CPF_OutParm類型且不為CPF_ReturnParm就將入?yún)⒅袑?yīng)的值復(fù)制到FuncParamsStructAddr中。在這里為每個需要的屬性分配了FOutParmRec,從而去構(gòu)建Frame.OutParam的屬性鏈表。
????通過Property->ContainerPtrToValuePtr<void*>(FuncParamsStructAddr)可以得到該屬性的指針偏移,從而把入?yún)⒅蟹莄onst 引用參數(shù)復(fù)制到參數(shù)內(nèi)存。FProperty有Property->GetOffset_ForInternal(), Property->GetSize()等方法可以確定對應(yīng)參數(shù)內(nèi)存大小。
????參數(shù)準(zhǔn)備完成之后,開始調(diào)用方法并處理返回值。
? ? Invoke第一個參數(shù)為Function所在的UClass,第二個參數(shù)為上面構(gòu)建的Frame,第三個參數(shù)為接收返回值的內(nèi)存地址。在本代碼中,統(tǒng)一用參數(shù)內(nèi)存的返回值部分進行接收。ReturnValueAddress是內(nèi)存參數(shù)偏移到返回值部分的首地址。
如果不是NativeFunc
將調(diào)用ProcessEvent進行函數(shù)調(diào)用。準(zhǔn)備參數(shù)階段是一樣的。
處理返回值
????第一行,取調(diào)用者傳入的返回值地址(TTuple)
????第二行,遍歷Function的屬性
????第四行,判斷是否為CPF_OutParm
????第五行,根據(jù)屬性的地址偏移,取到參數(shù)內(nèi)存中,該參數(shù)對應(yīng)的內(nèi)存地址
????第六行,將該內(nèi)存地址的值復(fù)制到返回參數(shù)中
????第七行,返回參數(shù)地址偏移屬性大小
????循環(huán)完成之后,已經(jīng)將所有的返回值復(fù)制到調(diào)用者傳入的TTuple中。
問題:
實測中發(fā)現(xiàn),如果有非const 類型的FString引用,當(dāng)處理函數(shù)中修改了引用值,可能會導(dǎo)致異常。
注意,調(diào)用時,最好加上Forward,否則會出問題。當(dāng)被調(diào)用的參數(shù)為引用類型時,TTuple展開之后也是引用,將會把數(shù)值的內(nèi)存地址傳給執(zhí)行函數(shù)而不是值本身。例如:
????上面定義的函數(shù),npcType和OutType為引用類型。通過下面的代碼去調(diào)用,會得到正確的結(jié)果,如果不加Forward,傳入?yún)?shù)就不為101和202
????

下面是兩種調(diào)用的完整代碼
? ??