UnLua解析(一)Object綁定lua

UnLua解析(一)Object綁定lua

https://zhuanlan.zhihu.com/p/100058725

相關文章:

南京周潤發:UnLua解析(二)使用Lua函數覆蓋C++函數

南京周潤發:UnLua解析(三)Lua訪問Object的property和function

南京周潤發:UnLua解析(四)數據在C++和lua間的相互傳遞

南京周潤發:UnLua解析(五)Delegate實現

簡介

UnLua是騰訊GCloud推出的lua組件,可以爲UE4賦予luab腳本開發能力。目前已開源,地址爲:https://github.com/Tencent/UnLua

本文作爲UnLua分析的第一部分,將介紹Object創建後與lua的綁定過程,這可以作爲理解UnLua的第一步,其中也包含了Class註冊與Function覆蓋等內容,這些是Object綁定的基礎條件。

UnLuaInterface

UnLua插件比較乾淨,接入UnLua,藍圖只需實現GetModuleName函數即可,這個函數返回一個lua文件的路徑,路徑相對於'Content/Script'。

比如Weapon/BP_DefaultProjectile_C.lua就是"Weapon.BP_DefaultProjectile_C"

GetModuleName函數聲明在UnLuaInterface中,可以用藍圖配置,也可以用C++類實現,UE中的類通過UnLuaInterface與lua進行關聯。

UObject和lua綁定

UnLua中Object綁定lua非常早,早到UObject剛創建時就綁定了。

FLuaContext實現FUObjectArray::FUObjectCreateListener接口,每當有UObjectBase創建時,會通過NotifyUObjectCreated收到通知。更深入瞭解一步,我們知道UObjectBase創建時,會在全局GUObjectArray數組中加入一個元素,正是在這裏發送的NotifyUObjectCreated通知。NotifyUObjectCreated中做的主要工作就是綁定lua,可以看FLuaContext::TryToBindLua函數。

首先做Editor中的判斷,可見Editor中創建的UObject會直接略過,PIE中的纔行。

#if WITH_EDITOR
    if (GIsEditor && !bIsPIE)
    {
        return false;
    }
#endif

對於一個UObject,首先需要判斷它是否爲CDO或ArchetypeObject,我們不需要爲CDO和模板對象綁定lua。還要過濾掉UClass和UPackage,這些對象都不需要綁定lua。

綁定有兩種方式,靜態綁定和動態綁定,可以簡單理解爲如果該類實現了UnLuaInterface接口,就使用靜態綁定;如果使用Lua中的"SpawnActor"或"NewObject"接口創建對象,就能在參數中指定ModuleName,之後使用動態綁定,可以使一個沒有繼承UnLuaInterface的類也可以使用lua擴展功能。

靜態綁定

先看靜態綁定,個人覺得靜態綁定會更廣泛。如果該類實現了UnLuaInterface,就走靜態綁定。

首先,通過在CDO上調用ProcessEvent,實現調用GetModuleName方法,得到ModuleName。然後使用UUnLuaManager::Bind()函數進行綁定。

  • 註冊Class

綁定第一步爲註冊該UClass,由RegisterClass()方法實現,需要創建一個重要數據結構FClassDesc,它儲存了一些元信息,用於描述一個UClass/Ustruct/UEnum。

根據UStruct,ClassName信息創建FClassDesc,然後更新TMap<FName, FClassDesc*> Name2Classes和TMap<UStruct*, FClassDesc*> Struct2Classes,方便以後查詢。

之後還要獲取當前UStruct的所有父類,把這些父類都註冊一遍,創建父類的FClassDesc。

以UClass爲例,FClassDesc創建時,首先會設置關於UClass的基本信息,比如Name,Type,Size等。然後把該UClass實現的所有UInterface也通過RegisterClass註冊一遍。之後再初始化該類的FunctionCollection數據結構,該數據結構用於lua調用C++函數時默認參數自動填充。

設置元表

註冊Class時有一個重要的步驟,就是設置Class對應的元表信息,這樣之後lua table就可以訪問Uobject的屬性和方法了。

  • UClass綁定Lua module (關鍵)

接着UnLua會在lua中找到我們定義的lua Module,使用GetFunctionList方法得到Module中定義的所有lua方法名。遍歷也會包括Module的所有父類。得到的結果存儲於 TMap<FString, TSet<FName>> ModuleFunctions容器中,它是ModuleName與FunctionList的鍵值對,方便以後查找。

然後遍歷剛剛得到的所有lua函數,從中找出lua覆寫C++UFunction的函數,目前包括"BlueprintEvent"和"RepNotifyFunc",也就是說,藍圖中無法覆寫的RepNotify函數,在UnLua中可以直接覆寫。

接下來是關鍵的”hook“這些C++中要被覆寫的UFunction。

首先,需要判斷這個UFunction是這個UClass的還是它父類的,是UClass的則替換UFunction,是父類的則添加UFunction。

子類添加Ufunction

void UUnLuaManager::AddFunction(UFunction *TemplateFunction, UClass *OuterClass, FName NewFuncName)
{
    UFunction *Func = OuterClass->FindFunctionByName(NewFuncName, EIncludeSuperFlag::ExcludeSuper);
    if (!Func)
    {
        UFunction *NewFunc = DuplicateUFunction(TemplateFunction, OuterClass, NewFuncName); // duplicate a UFunction
        if (!NewFunc->HasAnyFunctionFlags(FUNC_Native) && NewFunc->Script.Num() > 0)
        {
            NewFunc->Script.Empty(3);                               // insert opcodes for non-native UFunction only
        }
        OverrideUFunction(NewFunc, (FNativeFuncPtr)&FLuaInvoker::execCallLua, GReflectionRegistry.RegisterFunction(NewFunc));   // replace thunk function and insert opcodes
        TArray<UFunction*> &DuplicatedFuncs = DuplicatedFunctions.FindOrAdd(OuterClass);
        DuplicatedFuncs.AddUnique(NewFunc);
#if ENABLE_CALL_OVERRIDDEN_FUNCTION
        GReflectionRegistry.AddOverriddenFunction(NewFunc, TemplateFunction);
#endif
    }
}

UnLua先把要覆寫的UFunction作爲TemplateFunction,新建了NewFunction。新建NewFunction通過DuplicateUFunction函數完成,會把TemplateFunction的Property逐個複製過去,然後Class把NewFunction添加到自己的FuncMap中,以後就能訪問了。

接下來會把NewFunc的字節碼清空,這就意味之後該TemplateFunction對應的藍圖邏輯執行不到了。

再看下面GReflectionRegistry.RegisterFunction函數調用,從名稱就能看出,是在註冊UFunction。類似UClass,UnLua也會對UFunction進行註冊,並創建FFunctionDesc作爲描述數據。FFunctionDesc數據結構也很重要,它可以作爲UFunction和LuaFunction之間的橋樑,Function指向UFunction,FunctionRef指向lua中的函數,還存有函數名,函數默認參數等信息。將來分析函數調用時會對它做詳細介紹。所有UFunction註冊信息位於GReflectionRegistry的Functions容器中。

UFunction邏輯覆蓋

創建完NewFunction並註冊後,需要進行UFunction覆蓋操作了,這一步也是UnLua中很重要的一點,可見OverrideUFunction函數,添加UFunction時bInsertOpcodes爲true,即總是添加字節碼。

/**
 * 1. Replace thunk function
 * 2. Insert special opcodes if necessary
 */
void OverrideUFunction(UFunction *Function, FNativeFuncPtr NativeFunc, void *Userdata, bool bInsertOpcodes)
{
    Function->SetNativeFunc(NativeFunc);
    if (Function->Script.Num() < 1)
    {
        if (bInsertOpcodes)
        {
            Function->Script.Add(EX_CallLua);
            int32 Index = Function->Script.AddZeroed(sizeof(Userdata));
            FMemory::Memcpy(Function->Script.GetData() + Index, &Userdata, sizeof(Userdata));
            Function->Script.Add(EX_Return);
            Function->Script.Add(EX_Nothing);
        }
        else
        {
            int32 Index = Function->Script.AddZeroed(sizeof(Userdata));
            FMemory::Memcpy(Function->Script.GetData() + Index, &Userdata, sizeof(Userdata));
        }
    }
}

這裏會把UFunction的C++函數指針和藍圖字節碼調用函數都指向FLuaInvoker::execCallLua函數,這樣不管調用純C++的RepNotify,還是blueprintevent,都能調用到execCallLua函數。在這裏UnLua專門添加了一個字節碼EX_CallLua,execCallLua則被聲明爲實現該字節碼的函數,同時也能作爲普通C++函數使用,可謂一舉兩得。execCallLua函數功能就和名字一樣,用於調用覆寫的lua函數,細節之後介紹。這裏可以發現UnLua在SHIPPING版本中會把FFunctionDesc直接拷貝到字節碼中作爲數據,非SHIPPING版本需要在execCallLua中根據UFunction去GReflectionRegistry.RegisterFunction找FFunctionDesc,應該是爲了在SHIPPING版本中加快運行速度,用空間換時間的思想。

子類替換Ufunction

/**
 * Replace thunk function and insert opcodes
 */
void UUnLuaManager::ReplaceFunction(UFunction *TemplateFunction, UClass *OuterClass)
{
    FNativeFuncPtr *NativePtr = CachedNatives.Find(TemplateFunction);
    if (!NativePtr)
    {
#if ENABLE_CALL_OVERRIDDEN_FUNCTION
        FName NewFuncName(*FString::Printf(TEXT("%s%s"), *TemplateFunction->GetName(), TEXT("Copy")));
        UFunction *NewFunc = DuplicateUFunction(TemplateFunction, OuterClass, NewFuncName);
        GReflectionRegistry.AddOverriddenFunction(TemplateFunction, NewFunc);
#endif
        CachedNatives.Add(TemplateFunction, TemplateFunction->GetNativeFunc());
        if (!TemplateFunction->HasAnyFunctionFlags(FUNC_Native) && TemplateFunction->Script.Num() > 0)
        {
            CachedScripts.Add(TemplateFunction, TemplateFunction->Script);
            TemplateFunction->Script.Empty(3);
        }
        OverrideUFunction(TemplateFunction, (FNativeFuncPtr)&FLuaInvoker::execCallLua, GReflectionRegistry.RegisterFunction(TemplateFunction));
    }
}

如果要lua要覆蓋的UFunction就在子類中,則需要替換該UFunction的邏輯,不能再創建同名函數了。

首先會拷貝一個名稱加上"Copy"後綴的NewFunc,NewFunc作爲原UFunction的備份。然後把它們加到TMap<UFunction*, UFunction*> OverriddenFunctions容器中,該容器存儲了原UFunction和CopyUFunction的鍵值對,之後有需要可以在裏面查找並調用原UFunction。

接着如果原UFunction有NativeFunc指針和字節碼,就把它保存到CachedNatives容器和CachedScripts中做記錄,用於以後恢復UFunction。畢竟在直接修改UFunction實例,在Editor中PIE結束不恢復,會導致UFunction內存壞掉。

保存信息後,就可以使用和上面相同的UFunction邏輯覆蓋步驟,修改NativeFunc指針和字節碼了,只不過這次直接操作的原UFunction。

 

  • 創建UObject對應的luatable

首先需要創建一個lua table,把它稱爲"INSTANCE"。

然後創建一個userdata,類型爲void*,其中存儲了UObject的指針,並且把該userdata類型設置爲twolevel_ptr。我們之前註冊Class時已經在lua中創建了該Class關聯的metatable,於是可以把剛創建的userdata的metatable設置上,這個userdata就能和UE對象系統相關聯了。我們把該userdata稱爲"RAW_OBJECT"

創建並初始化好userdata後,會在lua table上創建名爲"Object"的屬性,值就是userdata,即INSTANCET.Object = RAW_UOBJECT。這樣luatable就與UObject產生了關聯。

接着,獲取到Class對應的Module,就是GetModuleName()函數返回名稱對應的Module,爲Module創建"Overridden"屬性,值爲RAW_OBJECT的metatable。我們把該Module稱爲"REQUIRED_MODULE"。然後把REQUIRED_MODULE的metatable也設置爲RAW_OBJECT的metatable。

處理完Module後,就可以把INSTANCE的metatable設置爲REQUIRED_MODULE了。

這個流程有些複雜,光看文字敘述不太直接,下圖詳細展示了UObject和luatable的關係,以及如何產生聯繫的,主要方式就是metatable設置。

創建完lua table後,UnLua會在GObjectReferencer中記錄該Object,從而對該Object添加引用。然後在AttachedObjects中記錄Object與lua table的對應關係。

如果創建的是Actor,需要額外在AttachedActors中添加記錄。在Actor被刪除時會從AttachedActors裏刪除記錄。

然後,會檢查lua中是否有Initialize函數,lua中可以實現該函數做一些初始化工作,如果有就會調用。

動態綁定

如果在lua中使用"NewObject"和"SpawnActor",我們可以選擇指定提供ModuleName,這樣UnLua可以在運行時把一個UObject和ModuleName關聯起來,因此稱爲“動態”。

UObject與ModuleName關聯

以在lua中SpawnActor爲例,我們看下UObject如何關聯ModuleName。SpawnActor實現函數爲Uworld_SpawnActor,有如下代碼:

FScopedLuaDynamicBinding Binding(L, Class, ANSI_TO_TCHAR(ModuleName), TableRef);
AActor *NewActor = World->SpawnActor(Class, &Transform, SpawnParameters);
UnLua::PushUObject(L, NewActor);

可以看到,在創建Actor之前,創建了一個FScopedLuaDynamicBinding對象,會傳入Class,ModuleName,可選的InitializerTable參數。

FScopedLuaDynamicBinding::FScopedLuaDynamicBinding(lua_State *InL, UClass *Class, const TCHAR *ModuleName, int32 InitializerTableRef)
    : L(InL), bValid(false)
{
    if (L)
    {
        bValid = GLuaDynamicBinding.Setup(Class, ModuleName, InitializerTableRef);
    }
}

接着看其構造函數,構造函數中會使用全局的GLuaDynamicBinding對象進行設置。

bool FLuaDynamicBinding::Setup(UClass *InClass, const TCHAR *InModuleName, int32 InInitializerTableRef)
{
    if (!InClass || (Class && Class != InClass) || (ModuleName.Len() > 0 && ModuleName != InModuleName) || (!InModuleName && InInitializerTableRef == INDEX_NONE))
    {
        return false;
    }
    Class = InClass;
    ModuleName = InModuleName;
    InitializerTableRef = InInitializerTableRef;
    return true;
}

看下Setup函數,主要工作還是設置一下自己的Class等對象屬性。這樣在Object創建後,執行TryToBindLua時,就會知道這個對象的ModuleName已經記錄,可以動態綁定。

當然,從FScopedLuaDynamicBinding類的名稱就可以推測,它只會在這個作用域有效,觀察一下它的析構函數,會發現在其中做了GLuaDynamicBinding的清理,因此動態綁定只會對這個對象有效。

動態綁定剩下的流程與靜態綁定相同,都是註冊Class,綁定lua module,替換UFunction等。

編輯於 02-27

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章