UnLua源码分析(二)IUnLuaInterface

UnLua源码分析(二)IUnLuaInterface

上一篇:UnLua源码分析(一)初始化流程

在UnLua中,我们可以通过Lua来编写或者覆盖蓝图的逻辑。要实现这一步的关键是,C++或者蓝图的类需要实现IUnLuaInterface接口。

使用

在任意的Actor蓝图中,点击UnLua工具栏的Bind选项,UnLua就会自动为蓝图实现IUnLuaInterface接口。

然后,在蓝图中实现接口的GetModuleName方法,这个方法就是获取蓝图对应Lua文件的路径。

在UnLua工具栏中,点击Create Lua Template,就会在这个路径下自动生成一个Lua的模板文件,直接打开这个文件,就可以编写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方法:

C++ 复制代码
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方法。

C++ 复制代码
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)的实现中一定会调用到此方法:

C++ 复制代码
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函数负责最终的绑定逻辑,我们来看看这个函数的实现:

C++ 复制代码
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来访问这个类的属性和方法。

C++ 复制代码
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的构造函数,其中有一句

C++ 复制代码
UnLuaLib::Open(L);

来看下UnLuaLib::Open函数的实现,这里也只截取我们当前关心的部分:

C++ 复制代码
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函数的实现如下:

C++ 复制代码
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的代码:

C++ 复制代码
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函数来真正执行这个函数。

C++ 复制代码
/**
 * 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方法。这一套流程也被称之为静态绑定,有关静态绑定的各种细节,我们将在后续继续分析。

Reference

1\] [UnLua与UE4的UObject绑定原理深入分析](https://zhuanlan.zhihu.com/p/397437868) \[2\] [UnLua解析(一)Object绑定lua](https://zhuanlan.zhihu.com/p/100058725) \[3\] [UnLua_Programming_Guide](https://github.com/Tencent/UnLua/blob/master/Docs/CN/UnLua_Programming_Guide.md)

相关推荐
慢慢沉2 小时前
Lua(数据库访问)
开发语言·数据库·lua
慢慢沉2 小时前
Lua协同程序(coroutine)
lua
慢慢沉19 小时前
Lua元表(Metatable)
lua
慢慢沉2 天前
Lua(字符串)
开发语言·lua
慢慢沉2 天前
Lua(数组)
开发语言·lua
慢慢沉2 天前
Lua(迭代器)
开发语言·lua
慢慢沉2 天前
Lua基本语法
开发语言·lua
Feng.Lee2 天前
接口测试Postman工具高级使用技巧
功能测试·测试工具·lua·postman·可用性测试
三翼鸟数字化技术团队3 天前
鸿蒙平台运行Lua脚本
lua·harmonyos