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"

UnLua解析(一)Object绑定lua_第1张图片

GetModuleName函数声明在UnLuaInterface中,可以用蓝图配置,也可以用C++类实现,UE中的类通过UnLuaInterface与lua进行关联。

UnLua解析(一)Object绑定lua_第2张图片

UObject和lua绑定

UnLua中Object绑定lua非常早,早到UObject刚创建时就绑定了。

FLuaContext实现FUObjectArray::FUObjectCreateListener接口,每当有UObjectBase创建时,会通过NotifyUObjectCreated收到通知。更深入了解一步,我们知道UObjectBase创建时,会在全局GUObjectArray数组中加入一个元素,正是在这里发送的NotifyUObjectCreated通知。NotifyUObjectCreated中做的主要工作就是绑定lua,可以看FLuaContext::TryToBindLua函数。

UnLua解析(一)Object绑定lua_第3张图片

首先做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 Name2Classes和TMap Struct2Classes,方便以后查询。

之后还要获取当前UStruct的所有父类,把这些父类都注册一遍,创建父类的FClassDesc。

以UClass为例,FClassDesc创建时,首先会设置关于UClass的基本信息,比如Name,Type,Size等。然后把该UClass实现的所有UInterface也通过RegisterClass注册一遍。之后再初始化该类的FunctionCollection数据结构,该数据结构用于lua调用C++函数时默认参数自动填充。

设置元表

注册Class时有一个重要的步骤,就是设置Class对应的元表信息,这样之后lua table就可以访问Uobject的属性和方法了。

UnLua解析(一)Object绑定lua_第4张图片

  • UClass绑定Lua module (关键)

接着UnLua会在lua中找到我们定义的lua Module,使用GetFunctionList方法得到Module中定义的所有lua方法名。遍历也会包括Module的所有父类。得到的结果存储于 TMap> 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 &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 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设置。

UnLua解析(一)Object绑定lua_第5张图片

创建完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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(UnLua解析(一)Object绑定lua)