Lua_第22章 Debug 库

22Debug

      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,以此类 推。

 

22.1自省(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):

<pre name="code" class="csharp">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 Luafunction
             print(string.format("[%s]:%d", info.short_src, info.currentline))
         end
        level =level + 1 
      end
end

 

不难改进这个函数,使得 getinfo 获取更多的数据,实际上debug 库提供了一个改善 的版本 debug.traceback,与我们上面的函数不同的是,debug.traceback 并不打印结果, 而是返回一个字符串。

 

 22.1.1访问局部变量

       调用 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(如果变量 索引超出范围)

 

 22.1.2访问 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 localvariables
  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(用来访问正在调用的函数)的用法。

 

     22.2Hooks

        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 将当前正在执行的文件名信息加上去,使得跟踪器稍微精致点的:

</pre></div><pre name="code" class="csharp">function trace (event, line)
   local s = debug.getinfo(2).short_src
   print(s .. ":" .. line)
end
debug.sethook(trace, "l")


    22.3 Profiles 

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

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

local Counters = {}
local Names = {}

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

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

local function hook ()
   local f = debug.getinfo(2, "f").func
   if Counters[f] == nil then  -- firsttime `f' iscalled?
       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> luaprofiler 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 offthe 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 

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

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值