C#、C、Lua分别有不同的异常处理机制,在跨语言函数调用的时候,必须要正确的处理异常,否则会导致堆栈错误、内存泄露、程序崩溃等问题。tolua对此做了非常全面的安全处理,值得我们去学习。如果我们要自己去做一些C层面的扩展,也必须要对这些底层原理熟记于心,才能避免各种诡异问题。
本文作者游蓝海,未经许可禁止转载。
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类部分源码:
// 将错误处理函数和要调用的函数压栈
public int BeginPCall(int reference)
{
return LuaDLL.tolua_beginpcall(L, reference);
}
// 执行函数调用
public void PCall(int args, int oldTop)
{
if (LuaDLL.lua_pcall(L, args, LuaDLL.LUA_MULTRET, oldTop) != 0)
{
string error = LuaToString(-1);
throw new LuaException(error, LuaException.GetLastError());
}
}
// 清理堆栈
public void EndPCall(int oldTop)
{
LuaDLL.lua_settop(L, oldTop - 1);
}
LuaException同时记录了Lua层的异常信息和C#层的信息,如果要做异常上报和分析,需要了解LuaException的结构。
public class LuaException : Exception
{
public LuaException(string msg, Exception e = null);
//返回异常产生时的C#堆栈
public override string StackTrace;
//返回最近一次的Lua异常
public static Exception GetLastError();
}
构造函数参数 | 说明 |
---|---|
msg | 通常是错误原因加上Lua堆栈。如果是Lua端出了异常,msg为Lua端的错误原因和Lua堆栈。如果是C#端出了异常,msg是C#异常名称和Lua堆栈,如果要看C#端的堆栈,需要修改C#的toluaL_exception ,从异常对象中获取完整的异常信息。 |
e | 上一个异常对象。可以是C#异常,也可以是Lua抛出的异常。因为函数可能会递归调用,底层的异常需要一层层的向外抛。 |
C#方法在注册到Lua的时候,有两个步骤:
tolua_closure
),生成一个新的闭包函数,然后在注册到Lua表中。当Lua层调用C#函数的时候,也是两个步骤:
tolua_closure
(之前注册时生成的的闭包函数);tolua_closure
内部会调用真正的C# Wrap函数。如果调用C#的过程中出现了异常,则tolua_closure
会将这个C#的异常信息,用lua_error
抛给Lua。
C#函数在push给Lua的时候,会调用接口tolua_pushcfunction
,将执行结果标记和C#函数作为tolua_closure
的闭包参数(upvalue)。
tolua_pushcfunction源码:
//hack for luac, 避免luac error破坏包裹c#函数的异常块(luajit采用的是类似c++异常)
LUA_API int tolua_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);
return 0;
}
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来获取或设置值。
Lua中调用C#函数的时候,实际上调用的是闭包函数tolua_closure
。tolua_closure
取到tolua_pushcfunction
传入的闭包参数,也就是真正的C#函数指针,然后调用C#函数。
tolua_closure源码:
int tolua_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解释器
return lua_error(L);
}
return r;
}
C# Wrap函数都必须处理所有的C#异常,然后将异常信息通过toluaL_exception
传递给C层:
public static int toluaL_exception(IntPtr L, Exception e)
{
LuaException.luaStack = new LuaException(e.Message, e, 2);
return tolua_error(L, e.ToString());
}
tolua_error
向当前正在执行的C闭包函数写入错误标记,当前函数执行完毕后,由tolua_closure
来检查执行结果:
int tolua_error(lua_State *L, const char *msg)
{
// 将错误标记(true)赋值给第一个闭包变量
lua_pushboolean(L, 1);
lua_replace(L, lua_upvalueindex(1));
// 返回错误原因
lua_pushstring(L, msg);
return 1;
}