C#与Lua进行交互主要通过虚拟栈实现,栈的索引有两套机制,分为正数与负数,如果索引为正数,则1表示栈底,如果索引为负数,则-1表示栈顶。虚拟栈是Lua和C#之间交互的关键。Lua和C#都使用虚拟栈来保存函数的参数和返回值,通过压入和弹出栈顶元素来进行数据传递。中间语言是通过虚拟栈中的数据来执行代码的,因此虚拟栈的实现非常重要。

我们热更新使用的xlua是一个用于将Lua集成到C#中的工具,而LuaEnv是xLua中Lua虚拟机的一个封装类。

关于LuaEnv的get方法和set方法,它们的行为与Lua的全局变量读写操作类似。具体来说:

  • get方法会将指定的全局变量名对应的值压入Lua虚拟栈中。这个值可以是任何类型,比如数字、字符串、表等等。如果全局变量不存在,get方法将会把nil值压入栈中。调用方可以通过读取栈顶元素来获取这个值。
  • set方法会从Lua虚拟栈中弹出一个值,并将其赋给指定的全局变量名。如果全局变量不存在,则会创建一个新的全局变量并赋值。如果栈顶元素不是一个合法的Lua值,则会抛出一个异常。

C#调用Lua

  1. 过程:
    1. 由C#文件先调用Lua解析器底层DLL库(由C语言编写),再由DLL文件执行相应的Lua文件
    2. 由C#生成Bridge文件,Bridge调DLL文件,先调用Lua中DLL文件,由DLL文件执行DLL代码
  2. 详细过程:
    1. 首先,创建一个Lua虚拟机,并将需要调用的Lua文件加载到虚拟机中。
    2. 在Lua中注册需要被C#调用的函数,通过LuaState.LuaRegisterFunction()函数将这些函数注册到Lua虚拟机中。
    3. 在C#中调用Lua函数时,首先需要将参数依次压入虚拟栈中。
    4. 然后,通过LuaState.LuaGetFunction()函数获取需要调用的Lua函数的指针,并通过LuaState.LuaCall()函数调用Lua函数。
    5. 在Lua函数执行完成后,将返回值依次压入虚拟栈中。在C#中获取返回值时,可以通过LuaState.LuaToNumber()LuaState.LuaToString()等函数将虚拟栈中的值转换为C#中的类型。
  3. 实质:C#把请求或数据放在栈顶,然后Lua从栈顶取出该数据,在Lua中做出相应处理(查询,改变),然后把处理结果放回栈顶,最后C#再从栈顶取出Lua处理完的数据,完成交互。
  4. C#->Bridge->DLL->Lua OR C#->DLL(C)->Lua

Lua调用C#

Wrap方式(静态生成代码)

  1. 过程:首先C#源文件生成对应的Wrap(中间文件/适配文件)文件,Wrap文件把字段方法,注册到Lua虚拟机中(解释器Luajit),然后Lua通过Wrap就可以调C#了
  2. 详细过程:
    1. 首先,需要在C#中定义一个类,该类包含需要在Lua中访问的方法和属性。
    2. 然后,定义一个C#的接口,该接口包含一个方法PushToLua(),用于将C#对象转换为Lua的table。在该方法中,需要使用Lua API将C#对象的属性和方法转换为Lua table中的成员。
    3. 在C#中创建一个子类,该子类继承自C#类和C#接口,并实现接口中的PushToLua()方法。在该方法中,可以使用Lua API将C#对象的属性和方法转换为Lua table中的成员。
    4. 在Lua中,通过调用C#接口中的PushToLua()方法将C#对象转换为Lua的table,并将该table注册为Lua全局变量或者局部变量。这样,在Lua中就可以通过访问该table来访问C#对象的属性和方法了。
  3. 实质:Lua把请求或数据放在栈顶,然后C#从栈顶取出数据,在C#中做出相应处理(查询,改变),然后把处理结果放回栈顶,最后Lua再从栈顶取出C#处理完的数据,完成交互。
  4. Lua->Wrap->C#
  5. 可能产生的问题:
    1. 生成有宏定义的方法,如Editor会有问题
    2. 生成的代码,尤其使用il2cpp,会增大编译后的代码大小相应地增加包体大小,
  6. Wrap模式的优点是可以使得Lua程序可以方便地访问C#中的对象和方法,同时也可以保证程序的类型安全性。缺点是需要在C#中实现接口和子类,并且需要进行类型转换,使得代码的可读性和可维护性都会受到影响。

反射方式

  1. 当索引系统中的API、DLL库或者第三方库时,如果无法将代码的具体实现进行代码生成,可采用此方式实现交互,执行效率低,所以一般使用Wrap方式调用C#。
  2. 可能产生的问题:
    1. 拆装箱开销
    2. stripping引用的反射失效
    3. 泛型方法,触发JIT,引起IOS异常
    4. 失去静态检查好处
  3. 详细过程:
    1. 首先,Lua需要通过clr包加载C#程序集,然后通过反射机制获取需要调用的C#方法。
    2. 在Lua中调用C#方法时,需要将方法的参数依次压入虚拟栈中。
    3. 然后,调用LuaDLL.lua_pcall()函数来调用C#方法。在调用时,需要将C#方法的指针压入虚拟栈中。
    4. C#方法执行完成后,将返回值依次压入虚拟栈中。在Lua中获取返回值时,可以通过LuaDLL.lua_tonumber()LuaDLL.lua_tostring()等函数将虚拟栈中的值转换为Lua中的类型。

参考资料

Unity中C#与Lua的交互

如何实现两门语言互相调用

Lua与其他宿主语言交互原理剖析

C# 与 Lua 交互(XLua 机制)

C#与Lua交互过程及原理

【ToLua】C#和Lua的交互细节

LUA TABLE实现原理(5.3源码)

散列·开放定址法

ChatGPT