在cocos2d-x中我们经常会使用lua来实现很多的上层功能,如配置文件,界面,网络协议等。当LUA的代码量越来越大时,调试的需求也越来越多。虽然网络上已经出现了一些可以调试的IDE工具,如decade,ldt,等。但由于本人愚笨,decade在我手上容易崩溃,LDT的网络调试总是不理想。于是开始寻找其它的调试方法。
随着对cocos2d-x的了解越来越深入,我接触到了一段cocos2dx中的示例代码,为了方便阅读这里略做改动,详情见HelloLua工程:
--file: main.lua function __G__TRACKBACK__(msg) print("----------------------------------------") print("LUA ERROR: " .. tostring(msg) .. "\n") print(debug.traceback()) print("----------------------------------------") end function main() CCLuaLog("function main start") p[2] = 2 --这里写了一个错误用来触发错误处理 CCLuaLog("function main finished") end xpcall(main, __G__TRACKBACK__)
可以看出这个文件的用意非常简单,就是通过系统的xpcall函数来执行main函数,同时传入了一个错误处理函数__G__TRACKBACK__
当你在C++中执行这main.lua这个文件时,就会得出如下的输出信息
[LUA-print] ---------------------------------------- [LUA-print] LUA ERROR: ...2d-x-2.1.5\projects\Flyman\Resources\script/main.lua:12: attempt to index global 'p' (a nil value) [LUA-print] stack traceback: ...2d-x-2.1.5\projects\Flyman\Resources\script/main.lua:6: in function <...2d-x-2.1.5\projects\Flyman\Resources\script/main.lua:3> ...2d-x-2.1.5\projects\Flyman\Resources\script/main.lua:12: in function <...2d-x-2.1.5\projects\Flyman\Resources\script/main.lua:10> [C]: in function 'xpcall' ...2d-x-2.1.5\projects\Flyman\Resources\script/main.lua:16: in main chunk [LUA-print] ----------------------------------------
如上显示,他打印出了当前的堆栈信息,如果是刚接触LUA的人会知道,平时函数中出错我们都只有一个错误信息,就是:
...2d-x-2.1.5\projects\Flyman\Resources\script/main.lua:12: attempt to index global 'p' (a nil value)
但想知道堆栈情况就比较难了。如果一个调用频率很高的公用函数出错了,那就很难知道是哪一处调用出错了。非常不方便调试。
有了上面的发现后,出于好奇,我开始研究这个神奇的xpcal函数。翻阅LUA官方手册后发现:
xpcall(f, err)This function is similar to pcall, except that you can set a new error handler.xpcall calls function f in protected mode, usingerr as the error handler. Any error insidef is not propagated; instead,xpcall catches the error, calls theerr function with the original error object, and returns a status code. Its first result is the status code (a boolean), which is true if the call succeeds without errors. In this case,xpcall also returns all results from the call, after this first result. In case of any error,xpcall returns false plus the result fromerr.
一串英文,读起来很费力,意思是说xpcall就是嵌套的lua_pcall,同时传了错误处理函数。如果函数在执行过程中没有任何错误,那就返回true,否则就返回false。
但尼马问题就来了,函数的参数和返回值怎么办,难道调用xpcall时,函数就只能是无参和无返回值么?带着对xpcall的鄙视之情,我开始思考能否编写出更强大的xpcall函数呢?
焦点转移到了 int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc),因为xpcall就是用它实现的。查看原码可以了解到:
static int luaB_xpcall (lua_State *L) { int status; luaL_checkany(L, 2); lua_settop(L, 2); lua_insert(L, 1); /* put error function under function to be called */ status = lua_pcall(L, 0, LUA_MULTRET, 1); lua_pushboolean(L, (status == 0)); lua_replace(L, 1); return lua_gettop(L); /* return status + all results */ }
注意lua_pcall那一行,查看lua_pcall的函数原型得知。第2个参数0表示没有参数,第3个参数LUA_MULTRET表示多个返回值,第4个参数表示错误处理函数在栈的1号位上。而1号位上就是我们传入的那个错误处理函数
通过这样的分析大概就明白怎么回事了。我们在C++中调用LUA函数时就可以传入一个错误处理函数,这样就能达到DEBUG的需求。
接着我们了解一下lua_pcall()的官方解释文档:
int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc)
Calls a function in protected mode.
是的,在保护模式下调用lua function。把lua_pcall和lua_call对比一下,体会更深。我们继续往下看,关键的地方来了
If errfunc is 0, then the error message returned on the stack is exactly the original error message.Otherwise,errfunc is the stack index of anerror handler function. (In the current implementation, this index cannot be a pseudo-index.) In case of runtime errors, this function will be called with the error message and its return value will be the message returned on the stack bylua_pcall.
Typically, the error handler function is used to add more debug information to the error message, such as a stack traceback. Such information cannot be gathered after the return oflua_pcall, since by then the stack has unwound.
The lua_pcall function returns 0 in case of success or one of the following error codes (defined inlua.h):
LUA_ERRRUN: a runtime error.
LUA_ERRMEM: memory allocation error. For such errors, Lua does not call the error handler function.
LUA_ERRERR: error while running the error handler function.
注意上面红色的部分,错误处理函数的参数是指这个函数在栈中的序号!以前我也一直纠结这个参数应该如何填,尼马,没师傅教我啊,等我教别人的时候,我就告诉他:这参数一般不使用,想研究可以自己私下去学习。在此对那位学生表示抱歉。
那这个序号究竟是怎么回事呢?LUA栈顶和栈底的序号都是0,如果从栈底往栈顶数,那就是正数,如果从栈顶往栈底数,那就是负数。尼马,这点坑倒了多少LUA学子,搞的如此高端,一不小心就出错,最关键是代码里,一会是正数,一会是负数,弄的人是欲仙欲死。说白了,这个序号有两层含义,如果是正的,那请你从栈底往栈顶依次数,如果是负的,那请从栈顶往栈底数。
这样事情就简单了,我们来实现自己的xpcall函数,代码如下:
int top = lua_gettop(m_state); lua_getglobal(m_state, "__G__TRACKBACK__");//通过函数名,压入错误处理函数 if (lua_type(m_state, -1) != LUA_TFUNCTION) //尼马,居然找不到,那肯定是拼写错误! { CCLog("[LUA ERROR] can't find function <__G__TRACKBACK__>err"); lua_settop(m_state, top); return; } int errfunc = lua_gettop(m_state); //刚压入的函数,肯定是栈顶哦,那gettop就是他的索引 lua_getglobal(m_state, "main"); //查找要执行的函数 if (lua_type(m_state, -1) != LUA_TFUNCTION) { CCLog("[LUA ERROR] can't find function <main>err:%s", lua_tostring(m_state, -1)); lua_settop(m_state, top); return; } if (lua_pcall(m_state, 0, 0, errfunc) != 0) //执行函数main,参数0,返回值0,errfunc是错误处理函数 { lua_settop(m_state, top); return; } lua_settop(m_state, top);
执行上面的代码吧,如何?是不是获得了和xpcall一样的效果?理论上打印信息是一模一样的。最关键的,我们可以自由的传入函数参数,并获取返回值了。(如果不会传入参数和返回值,我会在另一篇博文中讲述)
带着成功的兴奋我们得冷静一下,其实在代码的DEBUG过程中,堆栈信息中可用于调试的信息是非常有限的。更关键的是函数内部的变量情况。比如在VS2010中我们得到堆栈后,最关键的应该是一些变量的斌值情况,有了这些我们才能快速的排查BUG。
看来我们还要进一步完善我们的调试函数啊,lua_pcall已经被我们榨干了,基本上不会有更多的利用价值。那让我们来看看那个__G__TRACKBACK__()函数,他是如何把堆栈信息打印出来的呢?
debug.traceback(),是的使用lua的debug库,既然他能打印出堆栈,那是否意味着他也能打印出文件名,函数名,行号,局部变量等呢?看看前人是否已经做过这方的工作,问下度娘吧。
local function tostringex(v, len) if len == nil then len = 0 end local pre = string.rep('\t', len) local ret = "" if type(v) == "table" then if len > 5 then return "\t{ ... }" end local t = "" for k, v1 in pairs(v) do t = t .. "\n\t" .. pre .. tostring(k) .. ":" t = t .. tostringex(v1, len + 1) end if t == "" then ret = ret .. pre .. "{ }\t(" .. tostring(v) .. ")" else if len > 0 then ret = ret .. "\t(" .. tostring(v) .. ")\n" end ret = ret .. pre .. "{" .. t .. "\n" .. pre .. "}" end else ret = ret .. pre .. tostring(v) .. "\t(" .. type(v) .. ")" end return ret end local function tracebackex(msg) local ret = "" local level = 2 ret = ret .. "stack traceback:\n" while true do --get stack info local info = debug.getinfo(level, "Sln") if not info then break end if info.what == "C" then -- C function ret = ret .. tostring(level) .. "\tC function\n" else -- Lua function ret = ret .. string.format("\t[%s]:%d in function `%s`\n", info.short_src, info.currentline, info.name or "") end --get local vars local i = 1 while true do local name, value = debug.getlocal(level, i) if not name then break end ret = ret .. "\t\t" .. name .. " =\t" .. tostringex(value, 3) .. "\n" i = i + 1 end level = level + 1 end return ret end local function tracebackAndVarieble(msg) print(tracebackex()) end __G__TRACKBACK__ = tracebackAndVarieble function main() CCLuaLog("function main start") <span style="color:#990000"> local a = 1 </span> p[2] = 2 --这里写了一个错误用来触发错误处理 CCLuaLog("function main finished") end
得到如上代码,我们将__G__TRACKBACK__指向一个新的错误处理函数,同时修改了main()函数,在函数体内部加入一个局部变量a,看是否能如我所愿将其打印出来。
这样我们再执行一下main函数看看。
[LUA-print] stack traceback: [[string "script/DebugTool.lua"]]:57 in function `` msg = ...2d-x-2.1.5\projects\Flyman\Resources\script/main.lua:8: attempt to index global 'p' (a nil value) (string) (*temporary) = function: 0512A6D8 (function) [...2d-x-2.1.5\projects\Flyman\Resources\script/main.lua]:8 in function `` a = 1 (number) (*temporary) = nil (nil) (*temporary) = nil (nil) (*temporary) = attempt to index global 'p' (a nil value) (string)
可以看到,a的值也会被打印出来。这样就大方便了我们的调试工作。
在实际工作中,这种方法会占用大量的控制台信息,这是他的一大弊端,大家可以自动选择错误处理函数平时只显示堆栈信息,到调试时,再打开。切记这个开关要做在LUA层中,不要做在C++层,否则改一次,还得重编程序,那就太痛苦了。
其实想获得更详细的DEBUG信息,那就需要你去了解LUA的debug库了,相信那里会给出更全面的信息。而本文是我自己个人的方法,比较简单易懂,基本上可以解决大部分的问题。