LUA与CSHARP交互
相互调用
原理: C#与Lua进行交互主要通过虚拟栈实现
C# Call Lua:
- 内存: 由C#先将数据放入栈中,由lua去栈中获取数据,然后返回数据对应的值到栈顶,再由栈顶返回至C#。
- 代码: C#生成Bridge文件,Bridge调dll文件(dll是用C写的库),先调用lua中dll文件,由dll文件执行lua代码
C#->Bridge->dll->Lua OR C#->dll->Lua
Lua Call C#:
- 内存: 先生成C#源文件所对应的Wrap文件或者编写C#源文件所对应的c模块,然后将源文件内容通过Wrap文件或者C模块注册到Lua解释器中,然后由Lua去调用这个模块的函数。
- 代码:先生成Wrap文件(中间文件/适配文件),wrap文件把字段方法,注册到lua虚拟机中(解释器luajit),然后lua通过wrap就可以调C#了
或者在config文件中添加相应类型也可以
unity下的lua框架
为了使基于unity开发的应用在移动平台能够热更新,我们嵌入了Lua虚拟机,将需要热更新的逻辑用lua实现。c#通过P/Invoke和lua交互(lua由ANSI C实现)。在这个过程中,由于数据的交换需要使用==lua提供的虚拟栈 #F44336==,不够简单高效,为了解决这个问题,我们引入了*lua框架(xlua、slua、ulua)来达到类似RPC式的函数调用、类原生对象式的对象访问以及高效的对象传递。
XLUA
可以把xlua的push API归为两类:一类是针对某种特定类型的push,暂且叫做LowLevelAPI;还有一类是基于LowLevelAPI封装的更上层的HighLevelAPI。
- 门面模式
使用HighLevelAPI时你只要简单的传入你想push的对象,HighLevelAPI会帮你找到最适合的LowLevelAPI调用,因为就算同一种类型的push方法,也可能有用户自定义的优化版本。而对于LowLevelAPI最终是需要调用xlua.dll中提供的C API来协调完成最终的工作。
LowLevelAPI
//using RealStatePtr = System.IntPtr; |
- 传递基元类型
基元类型为 Boolean、Byte、SByte、Int16、UInt16、Int32、UInt32、Int64、UInt64、UIntPtr、Char、Double、Single和IntPtr (对应的void*)。
void pushPrimitive(RealStatePtr L, object o) |
XLUA中:
//push一个int |
对于long,xlua定制:
//i64lib.c |
- 传递 object
public void Push(RealStatePtr L, object o) |
不管object是什么类型,最终的push都是使用:
LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) { |
为什么我们传给lua的对象是一个int类型(这里的key)?其实我们这里的key是我们要传递的c#对象的一个索引,我们可以通过这个索引找到这个c#对象。
当传递一个c#对象的时候,我们创建一个userdate,并把这个索引值赋给这个userdata。然后,lua在全局注册表中,有一张专门的表用来存放c#各种类型所对应的元表,而meta_ref就是当前这个对象所对应类型的元表的索引id,我们通过他找到对应的元表,就可以通过setmetatable来绑定操作这个对象的方法。最终lua就可以愉快的使用这个对象。
每种类型所对应的元表,是我们在push一种类型的对象之前,提前注册进来的,后面详述。
但是对于引用类型的对象,其生命周期是有可能超出当前的调用栈的(比如lua用一个变量引用了这个对象) 。这时,我们就不仅要能够通过这个key找到c#原始对象,还要通过这个key能够找到对应的lua代理对象。因此,对于引用类型,我们在lua中同样也要建立一套索引机制,这就是need_cache和cache_ref的作用:
static void cacheud(lua_State *L, int key, int cache_ref) { |
- 缓存
再回过头来看看c#中的索引和缓存机制:
在调用xlua_pushcsobj之前,所有object都会被放入一个对象的缓存池中ObjectTranslator.objects。而我们得到的key就是这个对象在缓存池中的下标。
- gc
对于引用类型,它的生命周期管理会略微复杂。mono和lua虚拟机有各自的gc系统,并且相互无法感知。当lua和c#同时引用一个对象时,我们需要能够保证对象生命周期的正确,不能一边还在引用,另一边却把它释放掉了。
这个过程是由lua的gc驱动的。我们把对象push到lua时,会缓存在c#的对象池中,所以是不会被mono的gc所释放掉,这样就保证了lua能够安全的持有c#对象。同时我们也会把这个对象的代理缓存到lua中,而lua中对象的缓存表是一个弱表,也就是说,当没有其他的lua引用这个对象时,lua的gc会把这个对象从lua的缓存中回收,而对象被gc回收的过程会触发这个对象的的__gc元方法。
而这个__gc元方法就会通知到c#这端,来告诉我们lua不再使用这个对象,我们可以把它从对象缓存池中移除。当没有其他c#对其的引用时,mono的gc就会正常的回收这个对象。
//StaticLuaCallback.cs |
- 元表
对于业务来说,我们只是单纯的把对象的索引传递过去,是远远不够的,我们还需要提供直接使用和操作对象的方法。前面我们提到,在我们把一个对象push到lua之前,我们会把对象类型所对应的元表提前注册到lua之中。这样在我们真正push一个对象时,就会用这个元表来设置操作这个对象的方法。
首先第一个问题就是,如何表示c#对象的类型?回过头来看看我们的Push函数,其中最重要的就是getTypeId:
首先会尝试从c#的类型缓存typeIdMap中检查是否已经注册过这种类型,如果没有的话,我们就需要为其生成一个type_id。
再从lua的类型缓存中用类型名来检索是否已经注册过这种类型,如果没有的话,意味着我们还没有为这种类型在lua中注册一个元表,继而通过TryDelayWrapLoader来生成这个类型的元表。
// |
- 传递c#函数
xlua通过lua_pushstdcallcfunction来push一个LuaCSFunction,其调用的时xlua.dll提供的xlua_push_csharp_function。
//LUADLL.cs |
最终提供给用户的是这两个接口:
internal void PushFixCSFunction(RealStatePtr L, LuaCSFunction func) |
这两个函数都做了一件事情,就是在LuaCSFunction函数push到lua之前,用另一个LuaCSFunction来包装了一层,用来做异常捕获。
和gc一样,mono和lua有自己的异常
两种索引方式的不同,使用在了不同的场景。
PushFixCSFunction()大量被用在我们静态生成的元表构造器中,做为默认需要支持的类型的元表,注册进lua,并永久存在。而Push()被大量使用在反射生成的元表之中,在使用完之后,可能就会被释放。
最后还有一个小细节,Push()中对IsStaticPInvokeCSFunction的函数没有加包装,因为这种类型的函数是我们静态生成的,在生成时,我们已经加入了异常捕获的代码,不需要再被捕获了。
可以看到,一个函数在被调用之前,进行了多次的包装,每次包装都附带了一些额外的功能,但又对原函数没有侵入。(函数式编程,面向切片编程)
- 其他push
//push一个lua在c#中的代理对象 |
LuaBase是c#对lua中特有的类型的封装。比如说LuaTable对应table、LuaFunction对应luafunction(此处不是luacfunction)。C#可以通过对应的类型去创建、操作一个lua原生对象。
所以,LuaBase只是一个lua对象在c#中的代理,我们push一个LuaBase其实是找到真正的lua对象,并push。
//重载push一个decimal,避免gc |
HighLevelAPI
对于HighLevelAPI,里面不包含具体的push实现,而是通过获取对象的类型,来选择性的调用类型所对应的具体push函数。
可以看作类似是编译器的函数重载功能
public void PushAny(RealStatePtr L, object o) |
-
顾名思义,PushAny()可以用来push所有的类型,可以被用在我们提前没法知道对象类型的地方。最典型的例子就是在反射生成元表时,我们动态的获取对象,通过PushAny()把类型未知的对象push到lua。
-
PushByType()是对PushAny()的封装,唯一的不同就是做了一个优化:
对于基元类型,不再调用pushPrimitive() (会有装箱/拆箱),而是通过查表的方式直接获取针对各个基元类型的直接push的方式。