Debug库并没有为你提供一个Lua的调试器,而是为你提供了编写个人调试器的基本功能。出于性能的考虑,调用基本功能的正式接口都是通过C API提供。在Lua中访问debug库的一种方式是通过Lua代码直接访问。
和其他库不同,你要很吝啬的使用debug库。首先,库中的很多功能都会影响性能。其次,它打破了Lua中一些颠扑不破的真理,比如在一个局部变量的作用域之外,你不能访问它。最后,你可以不会想在产品发布的最后版本中包含它们,或者你可能想出去它们。
Debug库包含两大函数:内省函数(introspective functions)和hooks(钩子)。内省功能允许我们监视运行程序的几个方面,比如它活动函数的栈,当前的执行行,以及局部变量的值和名称。钩子运行我们跟踪程序的执行。
Debug库的一个很重要的概念是栈深度(stack level),栈深度用一个数字表示当前活动函数的栈:函数调用的debug库是level 1,调用函数本身是level 2,等等。
内省函数
debug库中的主要内省函数是debug.getinfo。它的第一个参数可以是函数或者一个栈深度值。当你为一些函数foo,使用debug.getinfo(foo)的时候,你将得到一个table,table中存放着这个函数的一些数据。table有如下几个域:(译者注:不是在所有时候,table中都包含这些域。)
source: 函数定义的位置。如果函数定义在字符串中(通过loadstring载入),source就是string。如果函数定义在一个文件中,source表示为带有'@'前缀的文件名。
short_src: source的一个简短表示(最多60个字符),在错误信息时使用。
linedefined: 源码中,函数定义的首行行号。
lastlinedefined: 源码中,函数定义的最后一行的行号。
what: 函数的类型。选项为”Lua",如果函数foo是一个lua函数,"C"如果是一个C函数,或者是"main“如果是Lua程序的主要部分。
name: 一个合适的函数名字。
namewhat: 前面一个域的含义。可以是”global“,”local“,”method“,”field“或者”“(空字符串)。空字符串表示Lua没有找到该函数的名称。
nups: 该函数upvalue的个数。
activelines: 是一个table,包含该函数活动行的集合。活动行是相对于空行或者注解行而言的,包含代码的行。(通常用于设置断点。大多数调试器不允许你在非活动行设置断点,因为在调试模式下接触不到非活动行。)
func: 函数本身;详细请见后面。
当foo是一个C函数时,Lua包含它的信息比较少。对于大多数C函数而言,只包含几个有关的域what,name,namewhat。
当对一些数字n调用debug.getinfo(n)的时候,你会得到当前栈中,栈深度值对应的活动函数。例如n是1,你会得到正在执行的函数。如果n是0,你会得到一个C函数getinfo函数本身。如果n超出了栈的深度,debug.getinfo返回nil。当你用一个数字调用debug.getinfo,查询一个活动函数时,做为结果返回的table,包含一个外额的域,currentline,表示当前函数所在行。此外,func中包含深度值所对应的函数。
name域很特别。因为Lua中的函数是first-class,一个函数可能没有名字,或者有多个名字。Lua查找函数名的方法为:通过代码中调用此函数的情况来决定。此方法只有当用数字调用getinfo函数时才起作用,也就是说,只有当我们要查看一具体调用的函数信息时才起作用。
getinfo函数效率很低。效率是放在第二位的,重要的是Lua尽可能的让调试信息不影响程序的执行。但是为了获得更好的性能,getinfo提供了一个可选择的第二参数。由此,getinfo就不需要对用户提供他们不需要的数据,从而节省了时间。第二参数的格式是字符串,字符串中的每个字母对应要获取的一个域,根据如下的表:
'n' 获取name和namewhat 'f' 获取func 'S' 获取source, short_src, what, linedefined, 和lastlinedefined 'l' 获取当前行 ‘L’ 获取活动行 'u' 获取nup (upvalue的个数) |
下面的函数用于解释debug.getinfo,函数输出活动栈的回溯:
function traceback()
for level = 1, math.huge do
local info = debug.getinfo(level, "nSl")
if not info then break end
if info.what == "C" then -- is a C function?
print(level, "C function")
else
print(info.name,string.format("%d, [%s] : %d", level, info.short_src, info.currentline))
end
end
end
通过从getinfo中获取更多的数据,可以很容易的改进这个函数。实际上,debug库已经提供了一个改进的版本,traceback函数。和我们写的版本不同的是,debug.traceback函数没有输出结果,而是返回一个表示回溯结果的字符串(通常很长)。
访问局部变量
我们可以通过debug.getlocal监视任何活动函数的局部变量。这个函数有两个参数:你希望查询函数的栈深度值和变量索引。它返回两个值:变量的名称和变量的值。如果变量的索引大于活动变量的数量,getlocal返回nil。如果栈深度值非法,函数就会引发一个错误。(我们可以通过debug.getinfo来检查栈深度值的有效性。)
Lua按照局部变量在函数中出现的顺序给变量标号,只统计函数中在当前作用域的活动变量。例如,以下函数:
function foo(a, b)
local x
do local c = a - b end
local a = 1
while true do
local name, value = debug.getlocal(1, a)
if not name then break end
print(name, value)
a = a + 1
end
end
调用函数foo(10, 20)会得到:
a 10
b 20
x nil
a 4
索引是1的变量是a,b的索引是2,x的索引是3,另一个a是4. getlocal的位置在超出了变量c的作用域。同时name和value变量还没有在作用域里面(注意,局部变量只在它初始化以后的代码中才可见。)
从Lua 5.2开始负索引可以得到函数额外参数的信息:索引 -1 指向第一个额外参数,这情况下,参数的名字总是“(*vararg)”。
你也可以用debug.setlocal修改局部变量的值。它的头两个参数和getlocal的一样,分别是栈深度值和变量索引。第三个参数是要赋给这个变量的值。函数返回变量的名称,如果变量在作用域外就返回nil。
访问非局部变量
debug库提供一个Lua函数getupvalue,允许我们通过它来访问非局部变量。和局部变量不同,非局部变量被一个存在的函数引用,即使这个函数不处于活动状态(基本上指的是闭包)。因此函数的第一个参数不是栈深度值,而是一个函数(更准确的说是一个闭包)。第二个参数是变量索引。Lua按照非局部变量被函数头一次引用的顺序给他们标号,但这个顺序是不相关的,因为一个函数不能访问两个使用同一个名字的非局部变量。
你可以通过debug.setupvalue更新非局部变量。可能正如你想的那样,这个函数有三个参数:一个闭包,一个变量索引,和一个新值。和setlocal函数类似,setupvalue函数返回这个变量的名字,如果超出了索引变量的作用域就返回nil。
function getvarvalue(name, level)
local value
local found = false
level = (level or 1) + 1
for i = 1, math.huge do
local n, v = debug.getlocal(level, i)
if not n then break end
if n == name then
value = v
found = true
end
end
if found then return value end
-- try non-local variables
local func = debug.getinfo(level, "f").func
for i = 1, math.huge do
local n, v = debug.getupvalue(func, i)
if not n then break end
if n == name then return v end
end
-- not found; get value from the environment
local env = getvarvalue("_ENV", level)
return env[name]
end
代码24.1显示了访问调用函数的任何变量。level为函数要查找的栈深度值;+1是因为当前调用栈中包含了getvarvalue本身。函数getvarvalue开始的时候,试着查找局部变量。如果出现重名的局部变量,则选择索引值最大的那个;因此循环总是执行到最后。如果不能找到要查找的变量,然后就到非局部变量中找。为此,它用debug.getinfo调用闭包函数,然后遍历它的非局部变量(译者注:这里的闭包函数就是函数本身,用getupvalue得到上一层函数的局部变量,即本函数的非局部变量)。最后,如果非局部变量中还找不到这个变量,就会到全局变量里找:它以_ENV为参数递归调用自己,到环境中去找。
访问其他协程
debug库中的所有内省函数都可以将一个协程作为它们的第一个参数。由此它们可以在协程的外部对协程进行监视。例如下面的例子:
co = coroutine.create( function ()
local x = 10
coroutine.yield()
error("some error")
end)
coroutine.resume(co)
print(debug.traceback(co))
函数co会对协程co输出如下结果:
stack traceback:
[C]: in function 'yield'
temp:3: in function
跟踪信息并没有到达resume函数,因为协程和主程序运行在不同的栈上。
当协程引发一个错误时,它不会清除释放它的栈。这意味着我们可以在它发生错误之后监视它。接着上面的例子,如果我们重新resume上面的协程,它会触发一个错误:
print(coroutine.resume(co)) --> false temp:4: some error
我们会得到如下的回溯信息:
[C]: in function 'error'
temp:4: in function
我们也可以监视协程中的局部变量,即使在它错误以后:
print(debug.getlocal(co, 1, 1)) --> x 10