|
C#、C、Lua分别有不同的异常处理机制,在跨语言函数调用的时候,必须要正确的处理异常,否则会导致堆栈错误、内存泄露、程序崩溃等问题。tolua对此做了非常全面的安全处理,值得我们去学习。如果我们要自己去做一些C层面的扩展,也必须要对这些底层原理熟记于心,才能避免各种诡异问题。
本文作者游蓝海,未经许可禁止转载。
文章目录
C#调用Lua函数
C#调用Lua函数的时候,必须使用lua_pcall接口进行调用。因为lua_pcall会拦截C语言层面的异常,确保异常不会破坏掉C#解释器的堆栈。
lua_pcall API说明:- int lua_pcall (lua_State *L,int nargs,int nresults,int error);
复制代码参数名称 | 说明 | L | Lua解释器指针 | nargs | 参数数量 | nresults | 返回值数量 | error | 异常处理函数句柄。也就是处理函数在Lua栈上的索引 | 如果函数调用成功,返回0,并且在Lua栈push上nresults个结果(如果实际结果不足,会push nil);否则,返回相应的错误码,并且在Lua栈push上错误原因的字符串。不管是否执行成功,lua_pcall都会把push的函数和nargs个参数从栈顶清除。
如果指定了错误处理函数,在函数调用失败的时候,Lua会调用该错误处理函数来处理错误原因。通常情况下,我们可以传入debug.traceback函数,这样可以得到函数调用堆栈。
ToLua在BeginPCall的时候,会将错误处理函数traceback和要调用的函数压栈。在PCall的时候,如果函数调用出错,则向C#层面抛出异常LuaException。
C# LuaState类部分源码:- // 将错误处理函数和要调用的函数压栈publicintBeginPCall(int reference){return LuaDLL.tolua_beginpcall(L, reference);}// 执行函数调用publicvoidPCall(int args,int oldTop){if(LuaDLL.lua_pcall(L, args, LuaDLL.LUA_MULTRET, oldTop)!=0){string error =LuaToString(-1);thrownewLuaException(error, LuaException.GetLastError());}}// 清理堆栈publicvoidEndPCall(int oldTop){
- LuaDLL.lua_settop(L, oldTop -1);}
复制代码 LuaException
LuaException同时记录了Lua层的异常信息和C#层的信息,如果要做异常上报和分析,需要了解LuaException的结构。- publicclassLuaException:Exception{publicLuaException(string msg,Exception e =null);//返回异常产生时的C#堆栈publicoverridestring StackTrace;//返回最近一次的Lua异常publicstaticExceptionGetLastError();}
复制代码构造函数参数 | 说明 | msg | 通常是错误原因加上Lua堆栈。如果是Lua端出了异常,msg为Lua端的错误原因和Lua堆栈。如果是C#端出了异常,msg是C#异常名称和Lua堆栈,如果要看C#端的堆栈,需要修改C#的toluaL_exception,从异常对象中获取完整的异常信息。 | e | 上一个异常对象。可以是C#异常,也可以是Lua抛出的异常。因为函数可能会递归调用,底层的异常需要一层层的向外抛。 |
Lua调用C#方法
C#方法在注册到Lua的时候,有两个步骤:
先在C#层生成一个静态的C# Wrap函数,并传递给C语言层;然后在C语言层面经过一层函数包装(tolua_closure),生成一个新的闭包函数,然后在注册到Lua表中。
当Lua层调用C#函数的时候,也是两个步骤:
先调用C函数tolua_closure(之前注册时生成的的闭包函数);tolua_closure内部会调用真正的C# Wrap函数。
如果调用C#的过程中出现了异常,则tolua_closure会将这个C#的异常信息,用lua_error抛给Lua。
注册C#函数
C#函数在push给Lua的时候,会调用接口tolua_pushcfunction,将执行结果标记和C#函数作为tolua_closure的闭包参数(upvalue)。
tolua_pushcfunction源码:- //hack for luac, 避免luac error破坏包裹c#函数的异常块(luajit采用的是类似c++异常)
- LUA_API inttolua_pushcfunction(lua_State *L, lua_CFunction fn){// 第1个闭包参数:结果标记。为true表示函数执行过程中出了异常lua_pushboolean(L,0);// 第2个闭包参数:要包装的C#函数lua_pushcfunction(L, fn);// 生成闭包函数lua_pushcclosure(L, tolua_closure,2);return0;}
复制代码 lua_pushcclosure API说明:- void lua_pushcclosure (lua_State *L, lua_CFunction fn,int n);
复制代码参数名称 | 说明 | L | Lua解释器指针 | fn | C函数指针 | n | n个闭包变量(upvalue) | 生成一个新的闭包函数放到栈顶。可以指定n个闭包参数,参数需要先push到栈顶(第一个参数要第一个push)。在闭包函数中,通过函数lua_upvalueindex(index)来获取闭包参数的索引,然后调用其他lua api来获取或设置值。
调用C#函数
Lua中调用C#函数的时候,实际上调用的是闭包函数tolua_closure。tolua_closure取到tolua_pushcfunction传入的闭包参数,也就是真正的C#函数指针,然后调用C#函数。
tolua_closure源码:- inttolua_closure(lua_State *L){// 拿到闭包变量2,也就是真正的C#函数
- lua_CFunction fn =(lua_CFunction)lua_tocfunction(L,lua_upvalueindex(2));// 调用C#函数int r =fn(L);// 如果C#函数中出现异常if(lua_toboolean(L,lua_upvalueindex(1))){// 恢复异常标记,确保不会影响下一次函数调用lua_pushboolean(L,0);lua_replace(L,lua_upvalueindex(1));// 将异常抛给Lua解释器returnlua_error(L);}return r;}
复制代码 C# Wrap函数都必须处理所有的C#异常,然后将异常信息通过toluaL_exception传递给C层:- publicstaticinttoluaL_exception(IntPtr L,Exception e){
- LuaException.luaStack =newLuaException(e.Message, e,2);returntolua_error(L, e.ToString());}
复制代码 tolua_error向当前正在执行的C闭包函数写入错误标记,当前函数执行完毕后,由tolua_closure来检查执行结果:- inttolua_error(lua_State *L,constchar*msg){// 将错误标记(true)赋值给第一个闭包变量lua_pushboolean(L,1);lua_replace(L,lua_upvalueindex(1));// 返回错误原因lua_pushstring(L, msg);return1;}
复制代码 |
|