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
根据UStruct,ClassName信息创建FClassDesc,然后更新TMap
之后还要获取当前UStruct的所有父类,把这些父类都注册一遍,创建父类的FClassDesc。
以UClass为例,FClassDesc创建时,首先会设置关于UClass的基本信息,比如Name,Type,Size等。然后把该UClass实现的所有UInterface也通过RegisterClass注册一遍。之后再初始化该类的FunctionCollection数据结构,该数据结构用于lua调用C++函数时默认参数自动填充。
设置元表
注册Class时有一个重要的步骤,就是设置Class对应的元表信息,这样之后lua table就可以访问Uobject的属性和方法了。
文章图片
- UClass绑定Lua module (关键)
然后遍历刚刚得到的所有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
接着如果原UFunction有NativeFunc指针和字节码,就把它保存到CachedNatives容器和CachedScripts中做记录,用于以后恢复UFunction。毕竟在直接修改UFunction实例,在Editor中PIE结束不恢复,会导致UFunction内存坏掉。
保存信息后,就可以使用和上面相同的UFunction逻辑覆盖步骤,修改NativeFunc指针和字节码了,只不过这次直接操作的原UFunction。
- 创建UObject对应的luatable
然后创建一个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里删除记录。
【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
推荐阅读
- 一个人的旅行,三亚
- 一个小故事,我的思考。
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量
- 开学第一天(下)
- 一个人的碎碎念
- 2018年11月19日|2018年11月19日 星期一 亲子日记第144篇
- 遇到一哭二闹三打滚的孩子,怎么办┃山伯教育
- 第326天
- Y房东的后半生14
- 奔向你的城市