tangkuai 发表于 2023-8-18 13:08

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

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

glayivan 发表于 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
{
    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;
      }

      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
{
    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
{
    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
Learning Lua: 5 - Document for luaL_findtable() function
页: [1]
查看完整版本: lua 和 c# 之间通信的底层实现是怎样的?