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
文章图片

GetModuleName函数声明在UnLuaInterface中,可以用蓝图配置,也可以用C++类实现,UE中的类通过UnLuaInterface与lua进行关联。
UnLua解析(一)Object绑定lua
文章图片

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

首先做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
文章图片

  • 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
文章图片

创建完lua table后,UnLua会在GObjectReferencer中记录该Object,从而对该Object添加引用。然后在AttachedObjects中记录Object与lua table的对应关系。
如果创建的是Actor,需要额外在AttachedActors中添加记录。在Actor被删除时会从AttachedActors里删除记录。
【UnLua解析(一)Object绑定lua】然后,会检查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



















    推荐阅读