Lua异常处理

Lua代码运行过程中,可能会出现异常状态,比如非法地址访问、遇到未定义符号、或者断言失败等,由于异常出现的地方不定,所以我们需要用一些方法来获取异常信息,找到出现异常的原因。

在C语言代码中处理Lua脚本运行产生的异常:

要能在发生异常后可以在C语言代码中获取到异常信息,就必须通过lua_pcall,或者lua_pcallk函数来运行Lua代码,如果直接使用lua_call函数来运行Lua代码,当发生异常后,Lua解析器不会保留异常信息,而是会调用系统函数abort导致整个程序运行中止。

对于lua_pcalllua_pcallk这两个函数,前者实际上是一个宏,是对后者做了一下简单封装后的结果,后者用得不多,这里不再做介绍。故名思意,lua_pcall函数是以保护模式运行Lua代码,当Lua代码中遇到异常时,lua_pcall函数不会直接调用abort函数导致程序卡死,而是会进行异常处理,然后恢复运行Lua代码之前的状态,然后返回。

在调用lua_pcall函数前,可以通过lua_pushcfunction函数将自己的异常处理函数压栈,然后通过lua_pcall函数的参数指定异常处理函数在栈中所在的位置,然后当Lua脚本运行出错时,这个自定义的异常处理函数会被调用,这时候可以继续在自定义异常处理函数中调用luaL_traceback函数,这个函数会获取到更多的错误信息,然后将错误信息以一个字符串的形式压栈,然后打印栈中的信息就可以得到详细的错误信息了。

当然,这个自定义异常处理函数不是必须的,如果没有自定义异常处理函数,那么解析器会将一个稍微简单一点的错误信息以字符串的形式压栈,当lua_pcall函数返回时,判断它的返回值,如果不为LUA_OK,就可以通过以字符串的形式将栈底打印出来获取错误信息。但需要注意的是,这两种方法不可以同时使用,换一种说法,如果使用了自定义异常处理函数,那么当lua_pcall函数返回后,栈顶的值就不会包含错误信息,因为在调用自定义异常处理函数的过程中,栈顶的错误信息会被覆盖掉。

说那么多,还是得来点代码。

Main.c:

int main(void)

{

????lua_State* L = NULL;

????int status;

????L = luaL_newstate();

????luaL_openlibs(L);

????luaL_loadfile(L, "test_01.lua");

????status=lua_pcall(L, 0, LUA_MULTRET,0);

lua_remove(L, 1);/*将ExceptionHandle从栈中删掉,注意这里不用lua_pop,而是lua_remove是因为此时ExceptionHandle不一定在栈顶*/

????if (status != LUA_OK)

????{

????????/*将错误代码和栈顶的字符串打印出来*/

????????printf("error code:%d,msg:%s\r\n",status,lua_tostring(L,-1));

????}

????lua_close(L);

????return 0;

}

?

Test_01.lua

Hello()

?

运行结果:

Lua异常处理

?

上面的代码中,main函数通过lua_pcall函数运行Lua脚本,Lua脚本中调用Hello函数,但是Hello函数并没有被定义,所以在运行时必定会产生异常,这里没有使用自定义异常处理函数,而是等lua_pcall返回后再打印栈顶获取异常信息。(C语言不同,这种符号未定义的情况在Lua编译器编译的时候并不会报错,但运行的时候要还是找不到这个函数,那就不得不报错了。)

再试一下使用自定义异常处理函数的情况,这里Main.c修改如下,Lua代码不修改。

Main.c:

int ExceptionHandle(lua_State* L)

{

????const char* msg = lua_tostring(L, -1);

????luaL_traceback(L, L, msg, 1);

????printf("%s\r\n", lua_tostring(L, -1));

????return 0;

}

int main(void)

{

????lua_State* L = NULL;

????int status;

????L = luaL_newstate();

????luaL_openlibs(L);

????lua_pushcfunction(L, ExceptionHandle);

????luaL_loadfile(L, "test_01.lua");

????status=lua_pcall(L, 0, LUA_MULTRET,1);

lua_remove(L, 1);/*将ExceptionHandle从栈中删掉,注意这里不用lua_pop,而是lua_remove是因为此时ExceptionHandle不一定在栈顶*/

????if (status != LUA_OK)

????{

????????/*将错误代码和栈顶的字符串打印出来*/

????????printf("error code:%d,msg:%s\r\n",status,lua_tostring(L,-1));

????}

????lua_close(L);

????return 0;

}

?

运行结果:

Lua异常处理

?

这里首先调用lua_pushcfunction(L, ExceptionHandle),将自定义异常处理函数压栈,由于是第一次压栈,所以ExceptionHandle函数在栈中的位置为1,然后加载Lua文件,调用lua_pcall函数运行Lua脚本,注意这里调用lua_pcall函数时的第四个参数为1,这个1,就是自定义异常处理函数在栈中的位置。

看结果,在自定义异常处理函数中,首先获取栈顶的值,并将其转换成字符串,这个字符串就是前面说的,Lua解析器压入的简单错误信息,获取到这个字符串后,再调用luaL_traceback函数获取相信错误信息,最后打印栈顶字符串,将详细错误信息打印出来,到这里,运行结果中的第1、2、3行便是运行的结果,然后lua_pcall函数退出后,主函数试图再次打印栈顶字符串,但打印出来的确实null(不应该是烫烫烫吗。。。),为啥?因为这时候栈顶已经不包含错误信息了,其实,如果细心看,这里运行结果的第一行和上面打印的错误信息是一样的,因为luaL_traceback函数将简单错误信息和详细错误信息合在一起了

问题:

为啥用了自定义异常处理函数后,Lua解析器存放在栈中的简单错误信息会被覆盖掉,按道理,调用一个函数,被调用的函数不应该影响调用它的函数的栈才对,并且调用完函数后,栈的状态应该和调用前完全一样才对,这个让我纠结了很久,因为一开始按照这个思路,定义了自定义异常处理函数,却没有在异常处理函数中将栈中的信息打印出来,而是等lua_pcall退出后才打印,总是出不来结果,后来还是找到了问题。

首先当异常发生后,Lua解析器会做什么?要知道发生异常后Lua解析器会做啥,就得知道异常会在哪里产生,这个问题简单,当然是在Lua代码执行的某条字节码时产生,Lua解释器会通过void luaV_execute (lua_State *L)函数执行Lua字节码。

Lua异常处理

上图是前面Lua代码的汇编代码,只有3条,第一条找到Hello函数,第二条执行它,第三条指令返回。经过测试,异常在第二条指令执行的时候产生,执行第二条指令时,会调用luaD_precall来执行对应的函数(只有C函数是在这个函数里被直接调用的)luaD_precall代码如下(删除了部分代码)

int luaD_precall (lua_State *L, StkId func, int nresults) {

/* do something */

switch (ttype(func))

{/* 判断函数类型,是C函数还是Lua函数 */

case LUA_TCCL: /* C closure */

/* do something */

goto Cfunc;

case LUA_TLCF: /* light C function */

/* do something */

return 1;

default:

{ /* 既不是C函数,也不是Lua函数 */

/* do something */

tryfuncTM(L, func); /* try to get ‘__call‘ metamethod */

return luaD_precall(L, func, nresults); /* 如果发生异常,则不会执行到这里 */

}

}

}

我们要看的就是default分支,它会调用tryfuncTM函数去继续寻找元方法(这里存疑),在寻找元方法之前,该函数首先会检测func是不是一个函数,这里当然不是,当确定func不是函数后,tryfuncTM函数便会抛出异常,代码如下:

static void tryfuncTM (lua_State *L, StkId func)

{

const TValue *tm = luaT_gettmbyobj(L, func, TM_CALL);

/*do something*/

if (!ttisfunction(tm))

luaG_typeerror(L, func, "call");/*抛出异常*/

/*do something*/

}

接下来luaG_typeerror函数会函数会给出错误原因,然后将错误原因传给luaG_runerror函数,luaG_runerror函数调用luaG_addinfo函数添加文件名和行号,然后将这些信息以一个字符串的形式压栈,这个字符串就是前面说的简单错误信息,最后调用luaG_errormsg函数,而luaG_errormsg函数中,我们可以找到简单错误信息被覆盖的原因。

Lua异常处理

Lua异常处理

三个变量:文件名、行号、错误信息,组成了简单错误信息。

接下来看luaG_errormsg函数,这个函数解释了为啥简单错误信息会被自定义异常处理函数覆盖。

l_noret luaG_errormsg (lua_State *L)

{

if (L->errfunc != 0)

{ /* is there an error handling function? */

StkId errfunc = restorestack(L, L->errfunc);

setobjs2s(L, L->top, L->top - 1); /* move argument */

setobjs2s(L, L->top - 1, errfunc); /* push function */

L->top++; /* assume EXTRA_STACK */

luaD_callnoyield(L, L->top - 2, 1); /* call it */

}

luaD_throw(L, LUA_ERRRUN);

}

首先这个函数会判断L->errfunc是否为0,这这个errfunc和lua_pcall的第4个参数有关,具体关系为L->errfunc=16*arg;(16为栈中一个元素的大小,arg为第四个参数值),如果arg不为0,则L->errfunc也不为0,这表示设置了自定义异常处理函数,进入if后,会调用restorestack找到压倒栈中的异常处理函数,然后调用setobjs2s调整栈数据,setobjs2s(L,A,B)就是将L的栈中,B地址指向的变量中数据给A地址指向的变量,注意,这里L->top-1地址指向的数据包含了简单错误信息,调整完后会调用luaD_callnoyield函数来掉用异常处理函数,画个图吧。

Lua异常处理

从上图可以看出,当执行完调整栈数据的三行代码后,简单错误信息被往上移了一格,而当 luaD_callnoyield(L, L->top - 2, 1);执行完后,栈指针会还原到调整栈数据之前的状态,这就回到了上图中的第二步的那种状态,由于Lua中的栈是空递增的栈,所以这时候简单错误信息无法被访问到,这里做一个小测试,将栈指针自己强行加1,看能不能打印出简单错误信息。

Lua异常处理

Lua异常处理

出来了,和预想的一样,但在写文档的时候又发现一个问题,在luaG_errormsg函数中对L->top强行加一可以得到预想中的结果,但在main函数中,等lua_pcall函数退出后再将L->top加一却出不来预想的结果,一顿跟踪,发现了问题的真相,lua_pcall函数经过层层调用,会调用到luaD_rawrunprotected函数,这个函数会继续调用其它函数来运行Lua脚本,函数的大致结构如下:

int luaD_pcall (lua_State *L, Pfunc func, void *u,ptrdiff_t old_top, ptrdiff_t ef)

{

int status;

/* do something:保存调用前的状态 */

L->errfunc = ef;

status = luaD_rawrunprotected(L, func, u);/*执行Lua脚本 */

if (status != LUA_OK)

{

/* 如果执行得有问题 */

/* do something:获取调用前的栈状态 */

seterrorobj(L, status, oldtop);

/* do something:还原调用前的状态 */

}

L->errfunc = old_errfunc;

return status;

}

当错误发生后,luaD_rawrunprotected函数的返回值是不等于LUA_OK的,也就是运行Lua脚本失败了,它会调用seterrorobj函数将错误信息移到调用前的栈指针所指向的那个个位置,然后将L->top强制设置为调用前栈指针oldtop+1seterrorobj函数代码如下:

static void seterrorobj (lua_State *L, int errcode, StkId oldtop)

{

switch (errcode)

{

case LUA_ERRMEM:

{

/* do something */

break;

}

case LUA_ERRERR:

{

/* do something */

break;

}

default:

{

setobjs2s(L, oldtop, L->top - 1); /* 将错误信息压倒旧栈指针指向的位置 */

break;

}

}

L->top = oldtop + 1;/* 强制恢复栈顶指针,并将其加一 */

}

?

可以看到,seterrorobj函数中有3个分支,分别是LUA_ERRMEMLUA_ERRERRdefault,这个分支代表Lua5.3参考手册中的5中错误代码:

  • LUA_OK (0): 成功。
  • LUA_ERRRUN: 运行时错误。
  • LUA_ERRMEM: 内存分配错误。对于这种错,Lua 不会调用错误处理函数。
  • LUA_ERRERR: 在运行错误处理函数时发生的错误。
  • LUA_ERRGCMM: 在运行__gc 元方法时发生的错误。这个错误和被调用的函数无关。

    画个图吧:

Lua异常处理

这是在luaG_errormsg函数中对L->top强制加一的情况。

Lua异常处理

这是在main函数中等lua_pcall函数退出后再将L->top加一的情况。

LUA_ERRMEMLUA_ERRERR有单独的分支,其它的错误代码都会进入default分支,调用seterrorobj函数。脚本中调用未定义函数属于运行时出错,所以也会进入default分支。这就解释了为啥在luaG_errormsg函数中对L->top强行加一可以得到预想中的结果,但在main函数中,等lua_pcall函数退出后再将L->top加一却出不来预想的结果,因为当luaG_errormsg函数退出后会做恢复原状态的工作,所以到main函数中的栈指针已经不是luaG_errormsg函数中的栈指针了,所以在main函数中对栈指针加一是不可行的。想想也应该,保护调用按道理也应该分三段:保存调用前状态、调用需要调用的函数、如果出错恢复出错前状态

至此,这个问题就理清了。