UnLua 函数覆写实现分析

概括来说:UnLua 绑定了 UE 创建对象的事件,当创建 CDO 时会调用到 UnLua 的 NotifyUObjectCreated,在其中拿到了该对象的 UClass,对该对象的 UClass 中的 UFUNCTION 通过SetNativeFunc 修改为 CallLua 函数,这样就实现了覆写 UFUNCTION。

下面来具体分析一下实现。UnLua 实现覆写完整的调用栈:

替换 Thunk 函数

在 UnLua 的 FLuaContext 的 initialize 函数中,将 GLuaCxt 注册到了 GUObjectArray 中:

1
2
3
4
5
6
// LuaContext.cpp
if (!bAddUObjectNotify)
{
GUObjectArray.AddUObjectCreateListener(GLuaCxt); // add listener for creating UObject
GUObjectArray.AddUObjectDeleteListener(GLuaCxt); // add listener for deleting UObject
}

FLuaContext 继承自 FUObjectArray::FUObjectCreateListenerFUObjectArray::FUObjectDeleteListener,所以当 UE 的对象系统创建对象的时候会把调用到 FLuaContext 的 NotifyUObjectCreatedNotifyUObjectDeleted

当创建一个 UObject 的时候会在 FObjectArrayAllocateUObjectIndex中对多有注册过的 CreateListener 调用 NotifyUObjectDeleted 函数。

而 UnLua 实现覆写 UFUNCTION 的逻辑就是写在 NotifyUObjectCreated 中的 TryBindLua 调用中,栈如下:

一个一个来说他们的作用:

FLuaContext::TryBindUnlua

1
2
// Try to bind Lua module for a UObject
bool FLuaContext::TryToBindLua(UObjectBaseUtility *Object);

主要作用是:如果创建的对象继承了 UUnLuaInterface,具有GetModuleName 函数,则通过传进来的 UObject 获取到它的 UCclass,然后再通过 UClass 得到 GetModuleName 函数的UFunction,并通过 CDO 对象调用该 UFunction,得到该 CLass 绑定的 Lua 模块名。

若没有静态绑定,则检查是否具有动态绑定。

UUnLuaManager::Bind

该函数定义在 UnLua/pRIVATE/UnLuaManager.cpp 文件中。

TryBindUnlua 中得到了当前创建对象的 UClass 和绑定的模块名,传递到了 Bind 函数中,它主要做了几件事情:

  1. 注册 Class 到 lua
  2. require 对应的 lua 模块
  3. 调用 UnLuaManager::BindInternal 函数
  4. 为当前对象创建一个 lua 端对象并 push 上一个 Initialize 函数并调用

BindInternal

其中的关键函数为UnLuaManager::BindInternal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Bind a Lua module for a UObject
*/
bool UUnLuaManager::BindInternal(UObjectBaseUtility *Object, UClass *Class, const FString &InModuleName, bool bNewCreated)
{
if (!Object || !Class)
{
return false;
}

lua_State *L = *GLuaCxt;
TStringConversion<TStringConvert<TCHAR, ANSICHAR>> ModuleName(*InModuleName);

if (!bNewCreated)
{
if (!BindSurvivalObject(L, Object, Class, ModuleName.Get())) // try to bind Lua module for survival UObject again...
{
return false;
}

FString *ModuleNamePtr = ModuleNames.Find(Class);
if (ModuleNamePtr)
{
return true;
}
}

ModuleNames.Add(Class, InModuleName);
Classes.Add(InModuleName, Class);

#if UE_BUILD_DEBUG
TSet<FName> *LuaFunctionsPtr = ModuleFunctions.Find(InModuleName);
check(!LuaFunctionsPtr);
TMap<FName, UFunction*> *UEFunctionsPtr = OverridableFunctions.Find(Class);
check(!UEFunctionsPtr);
#endif

TSet<FName> &LuaFunctions = ModuleFunctions.Add(InModuleName);
GetFunctionList(L, ModuleName.Get(), LuaFunctions); // get all functions defined in the Lua module
TMap<FName, UFunction*> &UEFunctions = OverridableFunctions.Add(Class);
GetOverridableFunctions(Class, UEFunctions); // get all overridable UFunctions

OverrideFunctions(LuaFunctions, UEFunctions, Class, bNewCreated); // try to override UFunctions

return ConditionalUpdateClass(Class, LuaFunctions, UEFunctions);
}

这个函数接受到的参数是创建出来的 UObject,以及它的 UClass,还有对应的 Lua 的模块名。

  1. 把对象的 UClass 与 Lua 的模块名对应添加到 ModuleNamesClasses
  2. 从 Lua 端通过 L 获取所指定模块名中的所有函数
  3. 从 UClass 获取所有的 BlueprintEvent、RepNotifyFunc 函数
  4. 对两边获取的结果调用 UUnLuaManager::OverrideFunctions 执行替换

UUnLuaManager::OverrideFunctions

对从 Lua 端获取的函数使用名字在当前类的 UFunction 中查找,依次对其调用UUnLuaManager::OverrideFunction.

UUnLuaManager::OverrideFunction

  1. 判断传入的 UFunction 是不是属于传入的 Outer UClasss
  2. 判断是否允许调用被覆写的函数
  3. 调用 AddFunction 函数

UUnLuaManager::AddFunction

  1. 如果函数为 FUNC_Native 则将 FLuaInvoker::execCallLua 和所覆写的函数名通过 AddNativeFunction 添加至 UClass
  2. UFunction 内的函数指针替换为(FNativeFuncPtr)&FLuaInvoker::execCallLua
  3. 如果开启了允许调用被覆写的函数,则把替换 NativeFunc 之前的 UFunction 对象存到 GReflectionRegistry

Call lua

首先,需要说的一点是,当使用 UEC++ 写的带有 UFUNCTION 并具有 BlueprintNativeEvent 或者 BlueprintImplementableEvent 标记的函数,UHT 会给生成对应名字的函数:

1
2
3
4
5
6
UFUNCTION(BlueprintNativeEvent,BlueprintCallable)
bool TESTFUNC();
bool TESTFUNC_Implementation();

UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "BeginPlay"))
bool TESTImplEvent(AActor* InActor,int32 InIval);

UHT 生成的函数和传递的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// generated.h
#define MicroEnd_423_Source_MicroEnd_423_Public_MyActor_h_13_EVENT_PARMS \
struct MyActor_eventReceiveBytes_Parms \
{ \
TArray<uint8> InData; \
}; \
struct MyActor_eventTESTFUNC_Parms \
{ \
bool ReturnValue; \
\
/** Constructor, initializes return property only **/ \
MyActor_eventTESTFUNC_Parms() \
: ReturnValue(false) \
{ \
} \
}; \
struct MyActor_eventTESTImplEvent_Parms \
{ \
AActor* InActor; \
int32 InIval; \
bool ReturnValue; \
\
/** Constructor, initializes return property only **/ \
MyActor_eventTESTImplEvent_Parms() \
: ReturnValue(false) \
{ \
} \
};

// gen.cpp
static FName NAME_AMyActor_TESTFUNC = FName(TEXT("TESTFUNC"));
bool AMyActor::TESTFUNC()
{
MyActor_eventTESTFUNC_Parms Parms;
ProcessEvent(FindFunctionChecked(NAME_AMyActor_TESTFUNC),&Parms);
return !!Parms.ReturnValue;
}
static FName NAME_AMyActor_TESTImplEvent = FName(TEXT("TESTImplEvent"));
bool AMyActor::TESTImplEvent(AActor* InActor, int32 InIval)
{
MyActor_eventTESTImplEvent_Parms Parms;
Parms.InActor=InActor;
Parms.InIval=InIval;
ProcessEvent(FindFunctionChecked(NAME_AMyActor_TESTImplEvent),&Parms);
return !!Parms.ReturnValue;
}

可以看到,UHT 帮我们定义了同名函数,并将其转发给ProcessEvent

注意:这里通过 FindFunctionChecked 方法是调用的UObject::FindFunctionChecked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UFunction* UObject::FindFunction(FName InName) const
{
return GetClass()->FindFunctionByName(InName);
}

UFunction* UObject::FindFunctionChecked(FName InName) const
{
UFunction* Result = FindFunction(InName);
if (Result == NULL)
{
UE_LOG(LogScriptCore, Fatal, TEXT("Failed to find function %s in %s"), *InName.ToString(), *GetFullName());
}
return Result;
}

可以看到,这里传递给 ProcessEventUFunction*就是从当前对象的 UClass 中得到的。

经过前面分分析可以知道,UnLua 实现的函数覆写,就是把 UClass 中的 UFunction 中的原生 thunk 函数指针替换为 FLuaInvoker::execCallLua,而且当一个对象的BlueprintNativeEventBlueprintImplementableEvent函数被调用的时候会调用到 ProcessEvent 并传入对应的 UFunction*,在ProcessEvent 中又调Invork(调用其中的原生指针),也就是实现调用到了 unlua 中替换绑定的FLuaInvoker::execCallLua,在这个函数中再转发给调用 lua 端的函数,从而实现了覆写函数的目的。