Lua_第22章 Debug 庫

heyuchang666發表於2016-04-06

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 己經很有用,並且可以作為很多高階工具的基礎。


相關文章