[轉]lua的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,以此類推。
23.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):
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並不打印結果,而是返回一個字符串。
23.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(如果變量索引超出範圍)
23.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 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(用來訪問正在調用的函數)的用法。
23.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")
23.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 -- 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已經很有用,並且可以作爲很多高級工具的基礎。
發佈了21 篇原創文章 · 獲贊 5 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章