找回密码
 立即注册
查看: 225|回复: 1

lua 和 c# 之间通信的底层实现是怎样的?

[复制链接]
发表于 2023-8-18 13:08 | 显示全部楼层 |阅读模式
lua 和 c# 之间通信的底层实现是怎样的?
发表于 2023-8-18 13:08 | 显示全部楼层
异次元的归来:tolua源码分析(二) C#调用lua函数的机制实现上一节我们讨论了C#是如何获取并调用到lua定义的函数,这一节我们更进一步,来看看如何让C#可以访问lua定义的变量的。依旧从一个例子看起,这次是tolua自带的example04,主要代码如下:
new LuaResLoader();
LuaState lua = new LuaState();
lua.Start();
lua["Objs2Spawn"] = 5;

string script =
@"
    print('Objs2Spawn is: '..Objs2Spawn)
    var2read = 42
    varTable = {1,2,3,4,5}
    varTable.default = 1
    varTable.map = {}
    varTable.map.name = 'map'

    meta = {name = 'meta'}
    setmetatable(varTable, meta)

    function TestFunc(strs)
        print('get func by variable')
    end
";

lua.DoString(script);

Debugger.Log("Read var from lua: {0}", lua["var2read"]);
Debugger.Log("Read table var from lua: {0}", lua["varTable.default"]);  

LuaFunction func = lua["TestFunc"] as LuaFunction;
func.Call();
func.Dispose();

LuaTable table = lua.GetTable("varTable");
Debugger.Log("Read varTable from lua, default: {0} name: {1}", table["default"], table["map.name"]);
table["map.name"] = "new";
Debugger.Log("Modify varTable name: {0}", table["map.name"]);

table.AddTable("newmap");
LuaTable table1 = (LuaTable)table["newmap"];
table1["name"] = "table1";
Debugger.Log("varTable.newmap name: {0}", table1["name"]);
table1.Dispose();

table1 = table.GetMetaTable();

if (table1 != null)
{
    Debugger.Log("varTable metatable name: {0}", table1["name"]);
}

object[] list = table.ToArray();

for (int i = 0; i < list.Length; i++)
{
    Debugger.Log("varTable[{0}], is {1}", i, list);
}

table.Dispose();                        
lua.CheckTop();
lua.Dispose();
第1-3行对lua虚拟机进行了初始化,这块内容我们在第一节里已经详细展开过,这里就不再赘述。通过第4行,可以发现C#的LuaState类重载了下标访问的操作符,使其扮演了lua中_G的角色,可以直接往lua层写入一个名为Objs2Spawn的变量,它的值为5。这一点在接下来执行的lua代码可以得到验证:




tolua源码分析(三) C#写入lua变量

下面就来看看下标访问操作符的具体实现,这里用到的是set,那么就先忽略掉get相关的实现:
public object this[string fullPath]
{
    get
    {
        ...
    }

    set
    {
        int oldTop = LuaGetTop();
        int pos = fullPath.LastIndexOf('.');

        if (pos > 0)
        {
            string tableName = fullPath.Substring(0, pos);
            IntPtr p = LuaFindTable(LuaIndexes.LUA_GLOBALSINDEX, tableName);

            if (p == IntPtr.Zero)
            {
                string name = fullPath.Substring(pos + 1);
                LuaPushString(name);
                PushVariant(value);
                LuaSetTable(-3);
            }
            else
            {
                LuaSetTop(oldTop);
                int len = LuaDLL.tolua_strlen(p);
                string str = LuaDLL.lua_ptrtostring(p, len);
                throw new LuaException(string.Format("{0} not a Lua table", str));
            }
        }
        else
        {
            PushVariant(value);
            LuaSetGlobal(fullPath);                    
        }

        LuaSetTop(oldTop);
    }
}
第10-11行就是判断是直接往lua的_G里赋值,还是往_G中的某个table里赋值。如果是往某个table中赋值,那么首先需要在lua层找到这个table,也就是这里的LuaFindTable,它的实现就是简单地调用lua的API函数luaL_findtable,值得一提的是如果查找的table不存在,则该函数会去创建对应name的table,这样大大简化了在C#层创建table的操作。如果函数执行成功,返回的是NULL。对应到第20-23行,此时lua栈顶是获取或创建的table,我们只需将key和value都压入栈,调用lua_settable设置即可。当然,如果是直接往_G里赋值,那直接调用lua_setglobal就行了。
这里我们稍微看一下PushVariant,它会调用到ToLua.Push上,这个方法就是根据C# object的类型,调用不同的push将object压入lua栈中。由于我们的参数是5,直接当作lua的number类型处理:
public static void Push(IntPtr L, object obj)
{
    if (obj == null || obj.Equals(null))
    {
        LuaDLL.lua_pushnil(L);
        return;
    }

    Type t = obj.GetType();

    if (t.IsValueType)
    {
        if (TypeChecker.IsNullable(t))
        {
            Type[] ts = t.GetGenericArguments();
            t = ts[0];
        }

        if (t.IsPrimitive)
        {
            double d = LuaMisc.ToDouble(obj);
            LuaDLL.lua_pushnumber(L, d);
        }

        ...
    }
    else
    {
        ...
    }
}
看完了set,我们回到例子中,第25-30行就用到了下标访问操作符的get了,相关输出如下:




tolua源码分析(三) C#访问lua变量

get的实现与set非常相似:
public object this[string fullPath]
{
    get
    {
        int oldTop = LuaGetTop();
        int pos = fullPath.LastIndexOf('.');
        object obj = null;

        if (pos > 0)
        {
            string tableName = fullPath.Substring(0, pos);

            if (PushLuaTable(tableName))
            {
                string name = fullPath.Substring(pos + 1);
                LuaPushString(name);
                LuaRawGet(-2);
                obj = ToVariant(-1);
            }   
            else
            {
                LuaSetTop(oldTop);
                return null;
            }
        }
        else
        {
            LuaGetGlobal(fullPath);
            obj = ToVariant(-1);
        }

        LuaSetTop(oldTop);
        return obj;
    }

    set
    {
        ...
    }
}
一个区别就是这里使用的是PushLuaTable,该函数的作用就是将C#指定的table压入到lua栈中,但是如果该table不存在,并不会新建,而是会返回false。
bool PushLuaTable(string fullPath, bool checkMap = true)
{
    if (checkMap)
    {
        WeakReference weak = null;

        if (funcMap.TryGetValue(fullPath, out weak))
        {
            if (weak.IsAlive)
            {
                LuaTable table = weak.Target as LuaTable;
                CheckNull(table, "{0} not a lua table", fullPath);
                Push(table);
                return true;
            }
            else
            {
                funcMap.Remove(fullPath);
            }
        }
    }

    if (!LuaDLL.tolua_pushluatable(L, fullPath))
    {               
        return false;
    }

    return true;
}
这个函数和上一节里说的PushLuaFunction类似,会先去查找C#缓存,如果缓存中存在,就直接通过缓存中记录的reference,调用lua_getref拿到对应的table,从而避免了每次要去解析字符串,递归查找table的过程。拿到table之后,再将key压入栈,就能得到value了。
同样地,这里有一个ToVariant,会根据lua栈上object的类型,调用不同的转换函数得到C#的object。例子中value的类型为number和function,对于number来说,直接调用lua_tonumber即可,而对于function来说,则稍微复杂一些:
public static object ToVarObject(IntPtr L, int stackPos)
{
    LuaTypes type = LuaDLL.lua_type(L, stackPos);

    switch (type)
    {
        case LuaTypes.LUA_TNUMBER:                    
            return LuaDLL.lua_tonumber(L, stackPos);
        case LuaTypes.LUA_TFUNCTION:
            return ToLuaFunction(L, stackPos);
        ...
        default:
            return null;
    }
}
public static LuaFunction ToLuaFunction(IntPtr L, int stackPos)
{
    LuaTypes type = LuaDLL.lua_type(L, stackPos);

    if (type == LuaTypes.LUA_TNIL)
    {
        return null;
    }

    stackPos = LuaDLL.abs_index(L, stackPos);
    LuaDLL.lua_pushvalue(L, stackPos);
    int reference = LuaDLL.toluaL_ref(L);
    return LuaStatic.GetFunction(L, reference);
}
通过这种方式,从lua栈上获取lua函数时,需要在lua层和C#层都进行缓存,因为这个函数相当于同时被lua层和C#层引用了。上一节中我们提到,lua层缓存的方式走的是toluaL_ref,得到reference后返回给C#层。然后,C#层会尝试先从缓存中查找reference,如果找到会增加其引用计数,找不到就将reference绑定到一个新的LuaFunction对象上:
LuaBaseRef TryGetLuaRef(int reference)
{            
    WeakReference weak = null;

    if (funcRefMap.TryGetValue(reference, out weak))
    {
        if (weak.IsAlive)
        {
            LuaBaseRef luaRef = (LuaBaseRef)weak.Target;

            if (luaRef.IsAlive)
            {
                luaRef.AddRef();
                return luaRef;
            }
        }               

        funcRefMap.Remove(reference);               
    }

    return null;
}

public LuaFunction GetFunction(int reference)
{
    LuaFunction func = TryGetLuaRef(reference) as LuaFunction;

    if (func == null)
    {               
        func = new LuaFunction(reference, this);
        funcRefMap.Add(reference, new WeakReference(func));
        if (LogGC) Debugger.Log("Alloc LuaFunction name , id {0}", reference);      
    }

    RemoveFromGCList(reference);
    return func;
}
下面一种方式,lua.GetTable是将lua层的table取出,C#层进行缓存的方式。注意它跟前面调用的PushLuaTable的区别。PushLuaTable这个函数不会增加缓存,也不会增加引用,这是因为它对应的table只是个临时产生的变量;而这里的GetTable并不同,因此获取的LuaTable会在lua层和C#层都进行一遍缓存,缓存的机制和前面类似,就不展开了。
public LuaTable GetTable(string fullPath, bool beLogMiss = true)
{
    WeakReference weak = null;

    if (funcMap.TryGetValue(fullPath, out weak))
    {
        if (weak.IsAlive)
        {
            LuaTable table = weak.Target as LuaTable;
            CheckNull(table, "{0} not a lua table", fullPath);

            if (table.IsAlive)
            {
                table.AddRef();
                RemoveFromGCList(table.GetReference());
                return table;
            }
        }

        funcMap.Remove(fullPath);
    }

    if (PushLuaTable(fullPath, false))
    {
        int reference = ToLuaRef();
        LuaTable table = null;

        if (funcRefMap.TryGetValue(reference, out weak))
        {
            if (weak.IsAlive)
            {
                table = weak.Target as LuaTable;
                CheckNull(table, "{0} not a lua table", fullPath);

                if (table.IsAlive)
                {
                    funcMap.Add(fullPath, weak);
                    table.AddRef();
                    RemoveFromGCList(reference);
                    return table;
                }
            }

            funcRefMap.Remove(reference);
        }

        table = new LuaTable(reference, this);
        table.name = fullPath;
        funcMap.Add(fullPath, new WeakReference(table));
        funcRefMap.Add(reference, new WeakReference(table));
        if (LogGC) Debugger.Log("Alloc LuaTable name {0}, id {1}", fullPath, reference);     
        RemoveFromGCList(reference);
        return table;
    }

    if (beLogMiss)
    {
        Debugger.LogWarning("Lua table {0} not exists", fullPath);
    }

    return null;
}
C#层的LuaTable,具有和lua层的table类似的操作,第33-35行展示了利用下标访问,对lua层的table读取和写入:




tolua源码分析(三) C#的LuaTable

LuaTable类的下标访问操作符key的类型只支持string和int,不过对于绝大部分情况已经完全够用。string类型和int类型的实现大同小异,这里以例子中用到的string为例,来看看具体的实现:
public object this[string key]
{
    get
    {
        int top = luaState.LuaGetTop();

        try
        {
            luaState.Push(this);
            luaState.Push(key);
            luaState.LuaGetTable(top + 1);
            object ret = luaState.ToVariant(top + 2);
            luaState.LuaSetTop(top);
            return ret;
        }
        catch (Exception e)
        {
            luaState.LuaSetTop(top);
            throw e;                    
        }               
    }

    set
    {
        int top = luaState.LuaGetTop();

        try
        {
            luaState.Push(this);
            luaState.Push(key);
            luaState.PushVariant(value);
            luaState.LuaSetTable(top + 1);
            luaState.LuaSetTop(top);
        }
        catch (Exception e)
        {
            luaState.LuaSetTop(top);
            throw e;
        }
    }
}
这个实现就是在C#层模拟了一遍对lua栈的操作,值得一提的是对LuaTable进行push,实际上等价于将lua层对应的table,push到lua栈顶,而不是真的push了一个C#对象到lua栈顶。因此,这里需要把LuaTable记录的reference取出,然后调用lua_getref到lua层取出真正的table:
public void Push(LuaBaseRef lbr)
{
    if (lbr == null)
    {               
        LuaPushNil();
    }
    else
    {
        LuaGetRef(lbr.GetReference());
    }
}
接着看例子的第37-41行,它展示了如何在C#层新建一个table。




tolua源码分析(三) C#层新建table

重点就在这个AddTable上,这个函数做的事情,其实也就是在C#层,模拟在lua栈上创建table的操作:
public void AddTable(string name)
{
    int oldTop = luaState.LuaGetTop();

    try
    {
        luaState.Push(this);
        luaState.Push(name);
        luaState.LuaCreateTable();               
        luaState.LuaRawSet(oldTop + 1);
        luaState.LuaSetTop(oldTop);
    }
    catch (Exception e)
    {
        luaState.LuaSetTop(oldTop);
        throw e;
    }
}
由于我们还需要把lua层新建的table转换成C#对应的LuaTable返回,因此这里还会用到之前提到的ToVarObject函数,在lua层生成reference,然后返回给C#层,查找对应的C#缓存,如果不存在就新建一个LuaTable对象。
public static object ToVarObject(IntPtr L, int stackPos)
{
    LuaTypes type = LuaDLL.lua_type(L, stackPos);

    switch (type)
    {
        case LuaTypes.LUA_TTABLE:
            return ToVarTable(L, stackPos);

        ...

        default:
            return null;
    }
}
接着看例子,下一个是在C#层去访问lua层设置的metatable。实现方式与前面所说的类似,还是C#层直接调用lua的API,模拟栈操作。




tolua源码分析(三) C#层访问metatable

例子的最后,演示了LuaTable.ToArray方法。通过这个方法,可以方便地将lua层的table转换为C#的数组。




tolua源码分析(三) LuaTable.ToArray

类似地,该方法的实现,就是在C#层,调用lua的lua_objlen函数,得到当前table的长度,然后在C#层对该table进行遍历,把结果塞到C#的数组中:
public object[] ToArray()
{
    int oldTop = luaState.LuaGetTop();

    try
    {
        luaState.Push(this);
        int len = luaState.LuaObjLen(-1);
        List<object> list = new List<object>(len + 1);
        int index = 1;
        object obj = null;

        while(index <= len)
        {
            luaState.LuaRawGetI(-1, index++);
            obj = luaState.ToVariant(-1);
            luaState.LuaPop(1);
            list.Add(obj);
        }               

        luaState.LuaSetTop(oldTop);
        return list.ToArray();
    }
    catch (Exception e)
    {
        luaState.LuaSetTop(oldTop);
        throw e;
    }
}
下一节我们将揭开lua层如何调用C#方法的面纱,先从C#的数组开始。
如果你觉得我的文章有帮助,欢迎关注我的微信公众号我是真的想做游戏啊

Reference
[1] Learning Lua: 5 - Document for luaL_findtable() function

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-3 08:06 , Processed in 0.099621 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表