UnLua源码分析(二)IUnLuaInterface
在UnLua中,我们可以通过Lua来编写或者覆盖蓝图的逻辑。要实现这一步的关键是,C++或者蓝图的类需要实现IUnLuaInterface
接口。
使用
在任意的Actor蓝图中,点击UnLua工具栏的Bind选项,UnLua就会自动为蓝图实现IUnLuaInterface
接口。
然后,在蓝图中实现接口的GetModuleName
方法,这个方法就是获取蓝图对应Lua文件的路径。
在UnLua工具栏中,点击Create Lua Template
,就会在这个路径下自动生成一个Lua的模板文件,直接打开这个文件,就可以编写Lua逻辑了。
一个最简单的Lua文件可能长这样:
local M = UnLua.Class()
-- 所有绑定到Lua的对象初始化时都会调用Initialize的实例方法
function M:Initialize()
local msg = "Hello World!"
print(msg)
end
return M
运行引擎,可以看到控制台输出了Hello World!
,说明Lua逻辑已经成功绑定到蓝图中了。
下面我们来分析一下UnLua是如何实现这个功能的。
尝试绑定
上一节我们提到,UnLua的入口是在FUnLuaModule
模块。这个模块它还继承自FUObjectArray::FUObjectCreateListener
这一接口,实现了NotifyUObjectCreated
方法:
virtual void NotifyUObjectCreated(const UObjectBase* ObjectBase, int32 Index) override
{
if (!bIsActive)
return;
UObject* Object = (UObject*)ObjectBase;
const auto Env = EnvLocator->Locate(Object);
Env->TryBind(Object);
}
也就是说,在每次创建UObject的时候,UnLua都会尝试绑定这个对象。EnvLocator->Locate(Object)
会返回一个FUnLuaEnv
对象,这个对象负责管理Lua虚拟机。由于Lua层的Initialize
方法会在对象创建时调用,所以可以猜测,UnLua会在TryBind
方法中,尝试绑定Lua模块,并且执行Initialize
方法。
bool FLuaEnv::TryBind(UObject* Object)
{
const auto Class = Object->IsA<UClass>() ? static_cast<UClass*>(Object) : Object->GetClass();
static UClass* InterfaceClass = UUnLuaInterface::StaticClass();
const bool bImplUnluaInterface = Class->ImplementsInterface(InterfaceClass);
if (!bImplUnluaInterface)
{
// dynamic binding
if (!GLuaDynamicBinding.IsValid(Class))
return false;
return GetManager()->Bind(Object, *GLuaDynamicBinding.ModuleName, GLuaDynamicBinding.InitializerTableRef);
}
const auto ModuleName = ModuleLocator->Locate(Object);
if (ModuleName.IsEmpty())
return false;
return GetManager()->Bind(Object, *ModuleName, GLuaDynamicBinding.InitializerTableRef);
}
TryBind
方法中首先判断了这个对象是否实现了IUnLuaInterface
接口,如果没有实现,就会使用动态绑定的方式进行绑定。所谓动态绑定,这是UnLua的一个特性,它允许将Lua模块绑定到运行时Spawn出来的Actor和Object,这一块我们后续再讨论。否则,就会通过ModuleLocator->Locate(Object)
获取到Lua模块的名称,然后调用GetManager()->Bind
方法进行绑定。那么相应地,这种方式就被称作为静态绑定。
上文中提到,IUnLuaInterface
接口的GetModuleName
方法会返回Lua模块的路径,那么显然就能猜测到,ModuleLocator->Locate(Object)
的实现中一定会调用到此方法:
FString ULuaModuleLocator::Locate(const UObject* Object)
{
const UObject* CDO;
if (Object->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject))
{
CDO = Object;
}
else
{
const auto Class = Cast<UClass>(Object);
CDO = Class ? Class->GetDefaultObject() : Object->GetClass()->GetDefaultObject();
}
if (CDO->HasAnyFlags(RF_NeedInitialization))
{
// CDO还没有初始化完成
return "";
}
if (!CDO->GetClass()->ImplementsInterface(UUnLuaInterface::StaticClass()))
{
return "";
}
return IUnLuaInterface::Execute_GetModuleName(CDO);
}
函数实现比较简单,就是先获取到合法的CDO对象,如果CDO对象没有初始化完成,或者没有实现IUnLuaInterface
接口,就返回空字符串。否则就调用GetModuleName
来获取Lua模块的名称。
真正绑定
绕来绕去,最后还是UUnLuaManager::Bind
函数负责最终的绑定逻辑,我们来看看这个函数的实现:
bool UUnLuaManager::Bind(UObject *Object, const TCHAR *InModuleName, int32 InitializerTableRef)
{
check(Object);
const auto Class = Object->IsA<UClass>() ? static_cast<UClass*>(Object) : Object->GetClass();
lua_State *L = Env->GetMainState();
if (!Env->GetClassRegistry()->Register(Class))
return false;
// try bind lua if not bind or use a copyed table
UnLua::FLuaRetValues RetValues = UnLua::Call(L, "require", TCHAR_TO_UTF8(InModuleName));
FString Error;
if (!RetValues.IsValid() || RetValues.Num() == 0)
{
Error = "invalid return value of require()";
}
else if (RetValues[0].GetType() != LUA_TTABLE)
{
Error = FString("table needed but got ");
if(RetValues[0].GetType() == LUA_TSTRING)
Error += UTF8_TO_TCHAR(RetValues[0].Value<const char*>());
else
Error += UTF8_TO_TCHAR(lua_typename(L, RetValues[0].GetType()));
}
else
{
BindClass(Class, InModuleName, Error);
}
if (!Error.IsEmpty())
{
UE_LOG(LogUnLua, Warning, TEXT("Failed to attach %s module for object %s,%p!\n%s"), InModuleName, *Object->GetName(), Object, *Error);
return false;
}
// create a Lua instance for this UObject
Env->GetObjectRegistry()->Bind(Class);
Env->GetObjectRegistry()->Bind(Object);
// try call user first user function handler
int32 FunctionRef = PushFunction(L, Object, "Initialize"); // push hard coded Lua function 'Initialize'
if (FunctionRef != LUA_NOREF)
{
if (InitializerTableRef != LUA_NOREF)
{
lua_rawgeti(L, LUA_REGISTRYINDEX, InitializerTableRef); // push a initializer table if necessary
}
else
{
lua_pushnil(L);
}
bool bResult = ::CallFunction(L, 2, 0); // call 'Initialize'
if (!bResult)
{
UE_LOG(LogUnLua, Warning, TEXT("Failed to call 'Initialize' function!"));
}
luaL_unref(L, LUA_REGISTRYINDEX, FunctionRef);
}
return true;
}
这个函数稍微复杂一些,大致可以分为以下几个步骤:
- 注册C++类的信息到Lua层
- 调用
require
函数加载Lua模块 - 将Lua模块绑定到C++
- 创建Lua instance,把上述所有信息绑定到instance
- 调用Lua模块的
Initialize
方法,如果存在的话
注册C++类
这一步骤的关键在FClassRegistry::Register
函数,这个函数会将C++类的信息注册到Lua的元表中,以便后续可以通过Lua来访问这个类的属性和方法。
FClassDesc* FClassRegistry::Register(const char* MetatableName)
{
const auto L = Env->GetMainState();
if (!PushMetatable(L, MetatableName))
return nullptr;
// TODO: refactor
lua_pop(L, 1);
FName Key = FName(UTF8_TO_TCHAR(MetatableName));
return Name2Classes.FindChecked(Key);
}
FClassDesc* FClassRegistry::Register(const UStruct* Class)
{
const auto MetatableName = LowLevel::GetMetatableName(Class);
return Register(TCHAR_TO_UTF8(*MetatableName));
}
如果使用的是蓝图类,LowLevel::GetMetatableName
返回的是蓝图资源的完整路径。
加载Lua模块
接下来我们自定义的Lua模块将会被加载。Lua模块里目前只有一个Initialize
函数,不过可以发现,Lua模板里有一句UnLua.Class()
,这个函数又是在哪里定义的呢?
让我们回到FLuaEnv
的构造函数,其中有一句
UnLuaLib::Open(L);
来看下UnLuaLib::Open
函数的实现,这里也只截取我们当前关心的部分:
static void LegacySupport(lua_State* L)
{
static const char* Chunk = R"(
local rawget = _G.rawget
local rawset = _G.rawset
local rawequal = _G.rawequal
local type = _G.type
local getmetatable = _G.getmetatable
local require = _G.require
local GetUProperty = GetUProperty
local SetUProperty = SetUProperty
local NotExist = {}
local function Index(t, k)
local mt = getmetatable(t)
local super = mt
while super do
local v = rawget(super, k)
if v ~= nil and not rawequal(v, NotExist) then
rawset(t, k, v)
return v
end
super = rawget(super, "Super")
end
local p = mt[k]
if p ~= nil then
if type(p) == "userdata" then
return GetUProperty(t, p)
elseif type(p) == "function" then
rawset(t, k, p)
elseif rawequal(p, NotExist) then
return nil
end
else
rawset(mt, k, NotExist)
end
return p
end
local function NewIndex(t, k, v)
local mt = getmetatable(t)
local p = mt[k]
if type(p) == "userdata" then
return SetUProperty(t, p, v)
end
rawset(t, k, v)
end
local function Class(super_name)
local super_class = nil
if super_name ~= nil then
super_class = require(super_name)
end
local new_class = {}
new_class.__index = Index
new_class.__newindex = NewIndex
new_class.Super = super_class
return new_class
end
_G.Class = Class
)";
luaL_loadstring(L, Chunk);
lua_newtable(L);
lua_getglobal(L, LUA_GNAME);
lua_setfield(L, -2, LUA_GNAME);
luaL_setfuncs(L, UnLua_LegacyFunctions, 0);
lua_setupvalue(L, -2, 1);
lua_pcall(L, 0, LUA_MULTRET, 0);
lua_getglobal(L, "Class");
lua_setfield(L, -2, "Class");
}
static int LuaOpen(lua_State* L)
{
lua_newtable(L);
luaL_setfuncs(L, UnLua_Functions, 0);
lua_pushstring(L, "Content/Script/?.lua;Plugins/UnLua/Content/Script/?.lua");
lua_setfield(L, -2, PACKAGE_PATH_KEY);
return 1;
}
int Open(lua_State* L)
{
luaL_requiref(L, "UnLua", LuaOpen, 1);
LegacySupport(L);
lua_pop(L, 1);
return 1;
}
Open
函数注册了UnLua
的全局模块,而LegacySupport
函数中定义了一个Class
函数,这个函数就是我们在Lua模板中使用的UnLua.Class()
。这里就是Lua的元表机制的应用,通过Class
函数,我们可以创建一个新的类,并且可以继承自其他类。
将Lua模块绑定到C++
有了Lua模块之后,我们就可以将Lua模块绑定到C++类上了。BindClass
函数的实现如下:
bool UUnLuaManager::BindClass(UClass* Class, const FString& InModuleName, FString& Error)
{
const auto L = Env->GetMainState();
const auto Top = lua_gettop(L);
if (!Class->IsChildOf<UBlueprintFunctionLibrary>())
{
// 一个LuaModule可能会被绑定到一个UClass和它的子类,复制一个出来作为它们的实例的元表
lua_newtable(L);
lua_pushnil(L);
while (lua_next(L, -3) != 0)
{
lua_pushvalue(L, -2);
lua_insert(L, -2);
lua_settable(L, -4);
}
}
lua_pushvalue(L, -1);
const auto Ref = luaL_ref(L, LUA_REGISTRYINDEX);
lua_settop(L, Top);
auto& BindInfo = Classes.Add(Class);
BindInfo.Class = Class;
BindInfo.ModuleName = InModuleName;
BindInfo.TableRef = Ref;
return true;
}
可以看到,加载的Lua模块table并不是直接拿来使用,而是复制了一份出来作为实例的类元表。这样做的好处是,Lua模块可以被多个C++类共享,而每个C++类都可以有自己的状态。被复制出来的table随后会记录到Lua的registry表中,C++层也会将这个table的ref存储在Classes
中。
创建Lua instance
如果我们在Initialize
方法中打印self
的type,会发现它是一个table。这个table就是Lua instance。UnLua的设计是不直接把C++对象以userdata的形式push到Lua层,而是用table做了一层封装。我们来看下负责创建Lua instance的代码:
int FObjectRegistry::Bind(UObject* Object)
{
const auto L = Env->GetMainState();
int OldTop = lua_gettop(L);
lua_getfield(L, LUA_REGISTRYINDEX, REGISTRY_KEY);
lua_pushlightuserdata(L, Object);
lua_newtable(L); // create a Lua table ('INSTANCE')
PushObjectCore(L, Object); // push UObject ('RAW_UOBJECT')
lua_pushstring(L, "Object");
lua_pushvalue(L, -2);
lua_rawset(L, -4); // INSTANCE.Object = RAW_UOBJECT
// in some case may occur module or object metatable can
// not be found problem
const auto Class = Object->IsA<UClass>() ? static_cast<UClass*>(Object) : Object->GetClass();
const auto ClassBoundRef = Env->GetManager()->GetBoundRef(Class);
int32 TypeModule = lua_rawgeti(L, LUA_REGISTRYINDEX, ClassBoundRef); // push the required module/table ('REQUIRED_MODULE') to the top of the stack
int32 TypeMetatable = lua_getmetatable(L, -2); // get the metatable ('METATABLE_UOBJECT') of 'RAW_UOBJECT'
if (TypeModule != LUA_TTABLE || TypeMetatable == LUA_TNIL)
{
lua_pop(L, lua_gettop(L) - OldTop);
return LUA_REFNIL;
}
lua_setmetatable(L, -2); // REQUIRED_MODULE.metatable = METATABLE_UOBJECT
lua_setmetatable(L, -3); // INSTANCE.metatable = REQUIRED_MODULE
lua_pop(L, 1);
lua_pushvalue(L, -1);
const auto Ret = luaL_ref(L, LUA_REGISTRYINDEX);
ObjectRefs.Add(Object, Ret);
lua_rawset(L, -3);
lua_pop(L, 1);
return Ret;
}
这段代码很长,但核心就做了两件事情。第一件事情就是创建了一个table作为Lua instance,然后把前面几个步骤中创建的对象都关联了起来。
如图所示,Lua class table就是通过require加载进来的Lua模块的复制表,UObject metatable就是在注册C++类过程中生成的元表,它既是表示原始UObject的userdata元表,也是Lua class table的元表。这意味着,
Lua instance table中字段的查找顺序,是先从Lua模块找起,找不到再去C++层找,这样倒是也挺合理。
第二件事就是存储了,Lua层会把instance table存到registry表中,并把UObject和instance table的关系,存到一个UnLua_ObjectMap
的表中。这个表的key是light userdata,也就是UObject指针,value则是registry表返回的ref。同样,C++层也类似,ObjectRefs
保存的也是同样的key和value。
调用Initialize
这一步就比较简单了,根据前面的分析,我们很容易就能猜测到,UnLua会在Lua层递归查找Initialize
方法,这里的PushFunction
函数会将Lua函数和Lua instance table压入栈顶,然后通过CallFunction
函数来真正执行这个函数。
/**
* Push a Lua function (by a function name) and push a UObject instance as its first parameter
*/
int32 PushFunction(lua_State *L, UObjectBaseUtility *Object, const char *FunctionName)
{
int32 N = lua_gettop(L);
lua_pushcfunction(L, UnLua::ReportLuaCallError);
const auto& Env = UnLua::FLuaEnv::FindEnv(L);
const auto Ref = Env->GetObjectRegistry()->GetBoundRef((UObject*)Object);
if (Ref != LUA_NOREF)
{
lua_rawgeti(L, LUA_REGISTRYINDEX, Ref);
int32 Type = lua_type(L, -1);
if (Type == LUA_TTABLE /*|| Type == LUA_TUSERDATA*/)
{
if (lua_getmetatable(L, -1) == 1)
{
do
{
lua_pushstring(L, FunctionName);
lua_rawget(L, -2);
if (lua_isfunction(L, -1))
{
lua_pushvalue(L, -3);
lua_remove(L, -3);
lua_remove(L, -3);
lua_pushvalue(L, -2);
return luaL_ref(L, LUA_REGISTRYINDEX);
}
else
{
lua_pop(L, 1);
lua_pushstring(L, "Super");
lua_rawget(L, -2);
lua_remove(L, -2);
}
} while (lua_istable(L, -1));
}
}
}
if (int32 NumToPop = lua_gettop(L) - N)
{
lua_pop(L, NumToPop);
}
return LUA_NOREF;
}
总结
UnLua会通过IUnLuaInterface
接口来获取蓝图对应的Lua模块路径,并在创建UObject时尝试绑定Lua模块,然后自动尝试执行Lua层的Initialize
方法。这一套流程也被称之为静态绑定,有关静态绑定的各种细节,我们将在后续继续分析。