unity tolua异常处理

C#、C、Lua分别有不同的异常处理机制,在跨语言函数调用的时候,必须要正确的处理异常,否则会导致堆栈错误、内存泄露、程序崩溃等问题。tolua对此做了非常全面的安全处理,值得我们去学习。如果我们要自己去做一些C层面的扩展,也必须要对这些底层原理熟记于心,才能避免各种诡异问题。

本文作者游蓝海,未经许可禁止转载。

文章目录

  • C#调用Lua函数
      • LuaException
  • Lua调用C#方法
      • 注册C#函数
      • 调用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类部分源码:

// 将错误处理函数和要调用的函数压栈
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

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抛出的异常。因为函数可能会递归调用,底层的异常需要一层层的向外抛。

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 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来获取或设置值。

调用C#函数

Lua中调用C#函数的时候,实际上调用的是闭包函数tolua_closuretolua_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;
}

你可能感兴趣的:(C#,Lua)