Lua Debug库(标准库相关)

Debug 库

debug 库并不给你一个可用的 Lua 调试器,而是给你提供一些为 Lua 写一个调试器的方便。出于性能方面的考虑,关于这方面官方的接口是通过 C API 实现的。Lua 中的debug 库就是一种在 Lua 代码中直接访问这些 C 函数的方法。Debug 库在一个 debug 表内声明了他所有的函数。

与其他的标准库不同的是,你应该尽可能少的使用 debug 库。首先,debug 库中的一些函数性能比较低;第二,它破坏了语言的一些真理(sacred truths),比如你不能在定义一个局部变量的函数外部,访问这个变量。通常,在你的最终产品中,你不想打开这个 debug 库,或者你可能想删除这个库:
debug = nil
debug 库由两种函数组成:自省(introspective)函数和 hooks。自省函数使得我们可以检查运行程序的某些方面,比如活动函数栈、当前执行代码的行号、本地变量的名和值。Hooks 可以跟踪程序的执行情况。

Debug 库中的一个重要的思想是栈级别(stack level)。一个栈级别就是一个指向在当前时刻正在活动的特殊函数的数字,也就是说,这个函数正在被调用但还没有返回。调用 debug 库的函数级别为 1,调用他(他指调用 debug 库的函数)的函数级别为 2,以此类推。

自省(Introspective)

在 debug 库中主要的自省函数是 debug.getinfo。他的第一个参数可以是一个函数或者栈级别。对于函数 foo 调用 debug.getinfo(foo),将返回关于这个函数信息的一个表。

这个表有下列一些域:
source,标明函数被定义的地方。如果函数在一个字符串内被定义(通过loadstring),source 就是那个字符串。如果函数在一个文件中定义,source 是@加上文件名。

short_src,source 的简短版本(最多 60 个字符),记录一些有用的错误信息。

linedefined,source 中函数被定义之处的行号。

what,标明函数类型。如果 foo 是一个普通得 Lua 函数,结果为 “Lua”;如果是一个 C 函数,结果为 “C”;如果是一个 Lua 的主 chunk,结果为 “main”。

name,函数的合理名称。

namewhat,上一个字段代表的含义。这个字段的取值可能为:W"global"、“local”、“method”、“field”,或者 “”(空字符串)。空字符串意味着 Lua 没有找到这个函数名。

nups,函数的 upvalues 的个数。

func,函数本身;详细情况看后面。

当 foo 是一个 C 函数的时候,Lua 无法知道很多相关的信息,所以对这种函数,只有 what、name、namewhat 这几个域的值可用。

以数字 n 调用 debug.getinfo(n)时,返回在 n 级栈的活动函数的信息数据。比如,如果 n=1,返回的是正在进行调用的那个函数的信息。(n=0 表示 C 函数 getinfo 本身)如果 n 比栈中活动函数的个数大的话,debug.getinfo 返回 nil。当你使用数字 n 调用debug.getinfo 查询活动函数的信息的时候,返回的结果 table 中有一个额外的域:currentline,即在那个时刻函数所在的行号。另外,func 表示指定 n 级的活动函数。

字段名的写法有些技巧。记住:因为在 Lua 中函数是第一类值,所以一个函数可能有多个函数名。查找指定值的函数的时候,Lua 会首先在全局变量中查找,如果没找到才会到调用这个函数的代码中看它是如何被调用的。后面这种情况只有在我们使用数字调用 getinfo 的时候才会起作用,也就是这个时候我们能够获取调用相关的详细信息。

函数getinfo 的效率并不高。Lua以不消弱程序执行的方式保存debug信息(Lua keeps debug information in a form that does not impair program execution),效率被放在第二位。为了获取比较好地执行性能,getinfo 可选的第二个参数可以用来指定选取哪些信息。指定了这个参数之后,程序不会浪费时间去收集那些用户不关心的信息。这个参数的格式是一个字符串,每一个字母代表一种类型的信息,可用的字母的含义如下:

'n' selects fields name and namewhat
'f' selects field func
'S' selects fields source, short_src, what, and linedefined
'l' selects field currentline
'u' selects field nup
下面的函数阐明了 debug.getinfo 的使用,函数打印一个活动栈的原始跟踪信息
(traceback):
function traceback ()
local level = 1
while true do
 local info = debug.getinfo(level, "Sl")
 if not info then break end
 if info.what == "C" then -- is a C function?
 print(level, "C function")
 else -- a Lua function
 print(string.format("[%s]:%d",
 info.short_src, info.currentline)) 
 end
 level = level + 1
end
end

不难改进这个函数,使得 getinfo 获取更多的数据,实际上 debug 库提供了一个改善的版本 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,2 是 b,3 是 x,4 是另一个 a。在 getlocal 被调用的那一点,c已经超出了范围,name 和 value 都不在范围内。(记住:局部变量仅仅在他们被初始化之后才可见)也可以使用 debug.setlocal 修改一个局部变量的值,他的前两个参数是栈级别和变量索引,第三个参数是变量的新值。这个函数返回一个变量名或者 nil(如果变量索引超出范围)

访问 Upvalues

我们也可以通过 debug 库的 getupvalue 函数访问 Lua 函数的 upvalues。和局部变量不同的是,即使函数不在活动状态他依然有 upvalues(这也就是闭包的意义所在)。所以,getupvalue 的第一个参数不是栈级别而是一个函数(精确的说应该是一个闭包),第二个
参数是 upvalue 的索引。Lua 按照 upvalue 在一个函数中被引用(refer)的顺序依次编号,因为一个函数不能有两个相同名字的 upvalues,所以这个顺序和 upvalue 并没什么关联(relevant)。

可以使用函数 ebug.setupvalue 修改 upvalues。也许你已经猜到,他有三个参数:一个闭包,一个 upvalues 索引和一个新的 upvalue 值。和 setlocal 类似,这个函数返回 upvalue的名字,或者 nil(如果 upvalue 索引超出索引范围)。

下面的代码显示了,在给定变量名的情况下,如何访问一个正在调用的函数的任意的给定变量的值:

function getvarvalue (name)
local value, found
-- try local variables
local i = 1
while true do
 local n, v = debug.getlocal(2, i)
 if not n then break end
 if n == name then
 value = v
 found = true
 end
 i = i + 1
end
if found then return value end
-- try upvalues
local func = debug.getinfo(2).func
 i = 1
while true do
 local n, v = debug.getupvalue(func, i)
 if not n then break end
 if n == name then return v end
 i = i + 1
end
-- not found; get global
return getfenv(func)[name]
end

首先,我们尝试这个变量是否为局部变量:如果对于给定名字的变量有多个变量,我们必须访问具有最高索引的那一个,所以我们总是需要遍历整个循环。如果在局部变量中找不到指定名字的变量,我们尝试这个变量是否为 upvalues:首先,我们使用debug.getinfo(2).func 获取调用的函数,然后遍历这个函数的 upvalues,最后如果我们找到给定名字的变量,我们在全局变量中查找。注意调用 debug.getlocal 和 debug.getinfo的参数 2(用来访问正在调用的函数)的用法。

Hooks

debug 库的 hook 是这样一种机制:注册一个函数,用来在程序运行中某一事件到达时被调用。有四种可以触发一个 hook 的事件:
当 Lua 调用一个函数的时候 call 事件发生;
每次函数返回的时候,return 事件发生;
Lua 开始执行代码的新行时候,line 事件发生;
运行指定数目的指令之后,count 事件发生。
Lua 使用单个参数调用 hooks,参数为一个描述产生调用的事件:“call”、“return”、“line” 或 “count”。另外,对于 line 事件,还可以传递第二个参数:新行号。我们在一个 hook 内总是可以使用 debug.getinfo 获取更多的信息。

使用带有两个或者三个参数的 debug.sethook 函数来注册一个 hook:第一个参数是hook 函数;第二个参数是一个描述我们打算监控的事件的字符串;可选的第三个参数是一个数字,描述我们打算获取 count 事件的频率。为了监控 call、return 和 line 事件,可以将他们的第一个字母(‘c’、‘r’ 或 ‘l’)组合成一个 mask 字符串即可。要想关掉 hooks,只需要不带参数地调用 sethook 即可。

下面的简单代码,是一个安装原始的跟踪器:打印解释器执行的每一个新行的行号:

debug.sethook(print, "l")

上面这一行代码,简单的将 print 函数作为 hook 函数,并指示 Lua 当 line 事件发生时调用 print 函数。可以使用 getinfo 将当前正在执行的文件名信息加上去,使得跟踪器稍微精致点的:

function trace (event, line)
local s = debug.getinfo(2).short_src
 print(s .. ":" .. line)
end 
debug.sethook(trace, "l")

Profiles

尽管 debug 库名字上看来是一个调式库,除了用于调式以外,还可以用于完成其他任务。这种常见的任务就是 profiling。对于一个实时的 profile 来说(For a profile with timing),最好使用 C 接口来完成:对于每一个 hook 过多的 Lua 调用代价太大并且通常会导致测量的结果不准确。然而,对于计数的 profiles 而言,Lua 代码可以很好的胜任。下面这部分我们将实现一个简单的 profiler:列出在程序运行过程中,每一个函数被调用的次数。

我们程序的主要数据结构是两张表,一张关联函数和他们调用次数的表,一张关联函数和函数名的表。这两个表的索引下标是函数本身。

--profiler.lua
local Counters = {}
local Names = {}

在 profiling 之后,我们可以访问函数名数据,但是记住:在函数在活动状态的情况下,可以得到比较好的结果,因为那时候 Lua 会察看正在运行的函数的代码来查找指定的函数名。

现在我们定义hook 函数,他的任务就是获取正在执行的函数并将对应的计数器加1;
同时这个 hook 函数也收集函数名信息:

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

下一步就是使用这个 hook 运行程序,我们假设程序的主 chunk 在一个文件内,并且用户将这个文件名作为 profiler 的参数:

prompt> lua profiler main-prog

这种情况下,我们的文件名保存在 arg[1],打开 hook 并运行文件:

local f = assert(loadfile(arg[1]))
debug.sethook(hook, "c") -- turn on the hook 
f() -- run the main program
debug.sethook() -- turn off the hook

最后一步是显示结果,下一个函数为一个函数产生名称,因为在 Lua 中的函数名不确定,所以我们对每一个函数加上他的位置信息,型如 file:line 。如果一个函数没有名字,那么我们只用它的位置表示。如果一个函数是 C 函数,我们只是用它的名字表示(他没有位置信息)。

function getname (func)
local n = Names[func]
if n.what == "C" then
 return n.name
end
local loc = string.format("[%s]:%s",
 n.short_src, n.linedefined)
if n.namewhat ~= "" then
 return string.format("%s (%s)", loc, n.name)
else
 return string.format("%s", loc)
end
end

最后,我们打印每一个函数和他的计数器:

for func, count in pairs(Counters) do
 print(getname(func), count)
end

如果我们将我们的 profiler 应用到 Section 10.2 的马尔科夫链的例子上,我们得到如下结果:

[markov.lua]:4 884723
write 10000
[markov.lua]:0 (f) 1
read 31103
sub 884722
[markov.lua]:1 (allwords) 1
[markov.lua]:20 (prefix) 894723
find 915824
[markov.lua]:26 (insert) 884723
random 10000
sethook 1
insert 884723 

那意味着第四行的匿名函数(在 allwords 内定义的迭代函数)被调用 884,723 次,write(io.write)被调用 10,000 次。

你可以对这个 profiler 进行一些改进,比如对输出排序、打印出比较好的函数名、改善输出格式。不过,这个基本的 profiler 已经很有用,并且可以作为很多高级工具的基础。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值