Chapter 23: The Debug Library
Debug 库没有给你一个lua 调试器,但是当你要写自已的调试器时,它提供了所有你需要的东西。因为效率的原因,官方接口是通过C API提供。Lua 中的调试库是一种在lua 代码中直接访问它们方法。不像其它的库,你应该吝啬的使用调试库。首先,是性能的原因。其次,它打破了语言的一些“哲学”,例如你不能从函数的外部访问一个由那个函数创建的局部变量。通常,你可能不希望在你的最终产品中打开这个库,或你可能想要擦除它,运行debug=nil。
调试库由两种函数组成:introspective functions 和 hooks。
Introspective 函数可以检查运行中的程序的许多信息,如活动函数的栈,当前执行行,局部变量的名字和值。Hooks 函数能够跟踪程序的执行。
调试库的一个重要概念是栈层级(stack level)。栈层级是一个与特殊函数相关联的数字,这个特殊函数是活动函数,所谓的活动函数就是已经被调用,并且还没有返回的函数。调用debug 库的那个函数的栈层级是1,调用“那个函数”的函数的栈层级是2,等等。
23.1 Introspective Facilities
debug.getinfo 是主要的introspective function 函数。它的第一个参数可以是一个函数或一个栈层级。当你调用debug.getinfo(foo),你获得一个有关这个函数的一些数据的表。这个表可以有以下域:
source:函数在哪里被定义。如果函数是在一个string 中定义(通过loadstring),source 是这个string。如果函数是在一个文件中定义,source 是这个文件的文件名,带有前缀“@”。
short_src: 一段被缩短了的源代码(最多60 字符),对错误消息有用。
linedefined: 函数是在哪一行开始定义。
lastlinedefined: 函数是在哪一行结束定义。
what: 这个函数是什么。对普通lua 函数是“lua”,C 函数是“C”,lua 的main chunk是“main”。
name: 有意义的函数名。
namewhat: 对name 域的解释。可能值是“global”,“local”,“method”,“field”或“”(空串)。空串意味着lua 找不到以那个名字命名的函数。
nups: 函数的upvalues(更新了多少个值?) 数。
activelines: 一个表,描述了函数的有效行(active line)的集。active line 是一行代码,不同于空行或只包含注释的行。(典型用法是用此信息来设置断点。多数调试器不允许在无效行上设置断点,因为它不可达)
func: 函数自身。
当传给debug.getinfo 的参数foo 是一个C 函数,lua 只会返回这个函数的少量信息。只有what,name,和namewhat 有效。
调用debug.getinfo(n),可以获得在第n 栈层级上的活动函数的数据。例如,如果n 是1,你会获得关于执行call 的那个函数的数据。(当n 是0,你得到的数据是关于getinfo 这个C 函数本身)。如果n 大于栈中的活动函数的个数,debug.getinfo 返回nil。当你调用debug.getinfo(n) 查询一个活动函数,返回的结果表有一个额外域,既当前行currentline,表示函数此刻运行到这一行了。此外,func 拥有在那个栈层级上活动的函数。
name 域是tricky。记得,因为lua 中函数是第一类型,一个函数可能没有名字,或可能有多个名字。Lua 试着去为函数找一个名字,进入被调用的函数的代码中,看看它是如何被调用的。这个方法仅当我们调用getinfo(n) 有效,就是说,我们获得有关一个特别invocation 的信息。
getinfo 函数是效率不高的。lua 将调试信息保存在一个form 中,它不impair 程序的执行。 在这里提高效率不是首要目标,而是第二目标。为获得更好的性能,getinfo 有一个可选的第二个参数,这个参数用来选择欲获取的信息。使用这个参数,getinfo 函数不会浪费时间收集那些用户不需要的数据。此参数是一个string,里面的每一个字母选择一组fields,对应于下表:
`n' selects name and namewhat
`f' selects func
`S' selects source, short_src, what, linedefined, and lastlinedefined
`l' selects currentline
`L' selects activelines
`u' selects nup
打印active stack 的traceback
function traceback ()
for level = 0, math.huge do
local info = debug.getinfo(level, "Sl")
if not info then break end
if info.what == "C" then -- is a C function?
print('level:'..level, "C function")
else -- a Lua function
print(string.format("level:%d [%s]:%d", level,info.short_src,
info.currentline))
end
end
end
traceback()
-- 当n 是0,你得到的数据是关于getinfo 这个C 函数本身
-- 当n 是1,你得到的数据是关于调用getinfo 这个函数的函数
要改进这个程序不难,通过从getinfo 插入更多数据。实际上,debug 库就有这么个改进版本,就是traceback 函数。和我们的版本不同,debug.traceback 不打印其结果,而是返回一个traceback string(通常很长)。
Accessing local variables
我们可以通过debug.getlocal 来检视任何活动函数的局部变量。此函数有两个参数:欲查询函数的栈层级和变量索引。返回值是:这个变量的变量名和当前值。如果变量索引大于活动变量数,getlocal 返回nil。如果栈层级无效,则引发错误。我们可以使用debug.getinfo 来检查栈层级的有效性。
Lua 以局部变量在函数中出现的先后顺序用1,2,3 ,4 … 来对局部变量的索引进行编号,并且只统计那些在函数的当前范围活动的局部变量。
局部变量的索引顺序
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(第一个参数),2 是b,3 是x,4 是另一个a。在函数getlocal 的调用点,c 已经超出范围。
你也可以改变局部变量的值,使用debug.setlocal。头两个参数分别是栈层级和变量索引。第三个参数是欲设置的新值。返回值是变量名或nil,如果变量索引超出范围。
Accessing non-local variables
debug 库也允许我们访问非局部变量,使用getupvalue 函数。和局部变量不同,由函数所引用的非局部变量一直存在,既使当函数不是活动的时也一样。所以,getupvalue 的第一个参数不是栈层级,而是一个函数(更准确的说是一个闭包)。第二个参数是变量索引。
Lua 以非局部变量在函数(既闭包)中被引用的先后顺序用1,2,3 ,4 … 来对非局部变量的索引进行编号,但这个顺序不是relevant 的,因为函数不能使用同一个名字访问两个非局部变量。
可以用debug.setupvalue 来更新非局部变量的值。像你所希望的那样,它有三个参数:一个闭包,一个变量索引,还有一个欲设置的新值。返回值是变量名,或nil,如果变量索引超出范围。
打印各栈层级上名字为“a”的变量
function getvarvalue (name) -- level 1
local value, found
-- try local variables
for i = 1, math.huge do
local n, v = debug.getlocal(2, i) -- level 0
if not n then break end
if n == name then
value = v
found = true
print(n, v)
end
end
if found then return value end
-- try non-local variables
local func = debug.getinfo(2, "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 from the environment
return getfenv(func)[name]
end
function foo (a, b) -- level 2
local x
do local c = a - b end
local a = 1 -- 这个a 索引是2,实参a 的索引是1
print(getvarvalue("a")) -- 打印具有最大索引的"a" -->1
end
foo(10, 20)
给定一个变量名,我们如何从一个正被调用的函数中获得这个变量的值。首先试着找局部变量,如果存在多个同名局部变量,我们就返回那个具有最高索引的。所以我们必须走完整个循环。如果找不到具有那个名字的局部变量,那么就试着去找非局部变量。首先,我们使用debu.getinfo 获得calling function,然后遍历它的非局部变量。如果非局变量也找不到,就在全局变量中找。注意使用数字2 作为第一个参数,还有debug.getinfo 去访问calling function。
Accessing other coroutines
debug 库的所有introspective 函数接受一个可选参数,既用协程作为第一个参数。所以,我们可以从外部检视协程。例如,考虑下面这个例子:
对协程进行traceback,getlocal
co = coroutine.create(function ()
local x = 10
coroutine.yield()
error("some error")
end)
coroutine.resume(co)
print(debug.traceback(co))
-->stack traceback:
--> [C]: in function 'yield'
--> a.lua:4: in function <a.lua:2>
print(coroutine.resume(co))
-->false a.lua:5: some error
print(debug.traceback(co))
-->stack traceback:
--> [C]: in function 'error'
--> a.lua:5: in function <a.lua:2>
print(debug.getlocal(co, 1, 1))
-->x 10
trace 不会go through 对resume 的调用,因为协程和主程序运行于不同的栈。如果协程引发一个错误,它不会展开它的栈。这意味着,我们可以在错误发生后检视它。继续我们的例子,如果我们再一次resume 协程,它遭遇了错误:
print(coroutine.resume(co)) --> false temp:4: some error
现在,如果我们打印它的traceback,我们得到这样的信息:
print(debug.traceback(co))
-->stack traceback:
--> [C]: in function 'error'
--> a.lua:5: in function <a.lua:2>
我们也可以检视一个协程的局部变量的值,既使是在发生错误之后:
print(debug.getlocal(co, 1, 1))
-->x 10
23.2 Hooks
debug 库的hook 机制允许我们注册一个函数(回调函数),程序运行过程中当一个特定事件发生时这个函数会被调用。有四种事件可以引发一个hook:call events,return events,line events,count events。其中call events 是在每次lua 调用一个函数时发生,return events 是在一个函数返回时发生,line events 是在lua 开始执行一行新代码时发生,count events 是在程序的执行到达一个给定的指令数后发生。lua 以单个参数调用hooks,既一个string,可以是“call”,“return”,“line”,或“count”。对line 事件,可以有第二个参数,既行号。要获得一个hook 内部的更多信息,我们必须使用debug.getinfo。
要注册一个hook,我们以两个或三个参数调用debug.sethook:第一个参数是作为hook 的函数(回调函数);第二个参数是一个string 表示欲监视的事件;第三个是可选参数,一个数字,表示我们希望以多大的频率获得count 事件。为监视call,return,line 事件,我们在mask string中添加它们的第一个字母(‘c’,‘r’,或‘l’)。为监视count 事件,我们简单偍供一个计数器作为第三个参数。要关闭hooks,我们以空参调用sethook。
打印lua 解释器执行的每行代码的行号
debug.sethook(print, "l")
这个调用简单的安装print 函数作为hook 函数,lua 会在line events 事件发生时调用它。
一个更精细的追踪器是使用getinfo 来获得文件名:
function trace (event, line)
local s = debug.getinfo(2).short_src
print(s .. ":" .. line)
end
debug.sethook(trace, "l")
co = coroutine.create(function ()
local x = 10
coroutine.yield()
error("some error")
end)
coroutine.resume(co)
print(debug.traceback(co))
-->stack traceback:
--> [C]: in function 'yield'
--> a.lua:4: in function <a.lua:2>
print(coroutine.resume(co))
-->false a.lua:5: some error
print(debug.traceback(co))
-->stack traceback:
--> [C]: in function 'error'
--> a.lua:5: in function <a.lua:2>
print(debug.getlocal(co, 1, 1))
-->x 10
23.3 Profiles
debug 库也可以用来完成debugging 以外的任务。一个常见的任务是进行profiling。这一节,我们将开发一个简单的性能分析器,用来列出程序中每个被调用的函数的执行时间。
用debug 库分析马尔可夫链算法的性能
-- input.txt
the more we try the more we do
---------------------
-- markov.lua
function allwords (f)
local line = f:read("*line") -- current line
local pos = 1 -- current position in the line
return function ()
while line do
local s, e = string.find(line, "%w+", pos)
if s then -- found a word?
pos = e + 1
return string.sub(line, s, e)
else
line = f:read("*line")
pos = 1
end
end
return nil
end
end
function prefix (w1, w2)
return w1 .. " " .. w2
end
local statetab = {}
function insert (index, value)
local list = statetab[index]
if list == nil then
statetab[index] = {value}
else
list[#list + 1] = value
end
end
f = io.open("c://input.txt","r") -- open input file
assert(f)
local N = 2
local MAXGEN = 10000
local NOWORD = "/n"
-- build table
local w1, w2 = NOWORD, NOWORD
for w in allwords(f) do
insert(prefix(w1, w2), w)
w1 = w2; w2 = w;
end
insert(prefix(w1, w2), NOWORD)
-- generate text
w1 = NOWORD; w2 = NOWORD -- reinitialize
for i=1, MAXGEN do
local list = statetab[prefix(w1, w2)]
-- choose a random item from list
local r = math.random(#list) -- Bug,这里的随机数是不随机的。
local nextword = list[r]
if nextword == NOWORD then return end
io.write(nextword, " ")
w1 = w2; w2 = nextword
end
f:close()
-------------------------------
-- a.lua
local Counters = {}
local Names = {}
local function hook ()
local f = debug.getinfo(2, "f").func
if Counters[f] == nil then -- first time 'f' is called?
Counters[f] = 1
Names[f] = debug.getinfo(2, "Sn")
else -- only increment the counter
Counters[f] = Counters[f] + 1
end
end
local f = assert(loadfile('markov.lua'))
debug.sethook(hook, "c") -- turn on the hook for calls
f() -- run the main program
debug.sethook() -- turn off the hook
function getname (func)
local n = Names[func]
if n.what == "C" then
return n.name
end
local lc = string.format("[%s]:%s", n.short_src, n.linedefined)
if n.namewhat ~= "" then
return string.format("%s (%s)", lc, n.name)
else
return lc
end
end
for func, count in pairs(Counters) do
print(getname(func), count)
end