一、调试库作用
使用调试库可以获取 Lua 环境运行过程中的变量和跟踪代码执行。
调试库主要分为两类函数:自省函数和钩子函数
- 自省函数:允许检查一个正在运行中的程序,例如活动函数的栈、当前正在执行的代码行、局部变量的名称和值。
- 钩子函数:允许跟踪一个程序的执行。
值得注意:
调试库的某些功能性能不高,而且会打破语言的一些固有规则。
二、自省函数
1、debug.getinfo(thread, f, what)
该函数会返回包含函数信息的表。
参数:
- thread: 表示在 thread 中获取相应的信息
- f : 可以给两种模式,函数或数值。第一种则是给函数,获取给定函数的信息表。第二种则是给一个数字作为
f
的值,表示栈层级:当为 0 时表示当前函数( 即getinfo
本身), 1 表示调用getinfo
的函数(尾调用除外,它们不计入堆栈),以此类推。如果f
是一个大于活跃栈层级的数字,则getinfo
返回 nil。 - what : 可选项,表示要获取哪些指定的信息。因为 getinfo 的效率不高,所以为了效率好些,可以只选择需要的内容,如果需要多个值时,可以将多个拼凑,例如
nfS
。
what 取值 | 获取的值 |
---|---|
n | 选择 name 和 namewhat |
f | 选择 func |
S | 选择 source、short_src、what、linedefined 和 lastlinedefined |
l | 选择 currentline |
L | 选择 activelines |
u | 选择 nup、nparams 和 isvararg |
这些字段的含义如下:
字段 | 描述 |
---|---|
name | 该字段是该函数的一个适当的名称,例如保存该函数的全局变量的名称。(可能没有值,也可能有多个名称)(只有当 f 为数值时才有该值) |
namewhat | 该字段用于说明 name 字段的含义,可能是 "global"、"local"、"method"、"field" 或 ""(空字符串)。空字符串表示 Lua 语言找不到该函数的名称。 |
func | 该字段是该函数本身 |
source | 该字段用于说明函数定义的位置。如果函数定义在一个字符串中(通过调用 load),那么 source 就是这个字符串;如果函数定义在一个文件中,那么 source 就是使用 @ 作为前缀的文件名 |
short_src | 该字段是 source 的精简版本(最多 60 个字符),对于错误信息十分有用。 |
what | 该字段用于说明函数的类型。 - 如果 foo 是一个普通的 Lua 函数,则为 "Lua" - 如果是一个 C 函数,则为 "C" - 如果是一个 Lua 语言代码段的主要部分,则为 "main" 。 |
linedefined | 该字段是该函数定义在源代码中第一行的行号 |
lastlinedefined | 该字段是该函数定义在源代码中最后一行的行号 |
currentline | 表示当前该函数正在执行的代码所在的行 (只有当 f 为数值时才有该值) |
istailcall | 返回一个布尔值,为真表示函数是被尾调用所调起(尾调用时,函数真正的调用者不在栈中)(只有 f 为数值时才有该值) |
activelines | 该字段是一个包含该函数所有活跃行的集合。活跃行是指除空行和只包含注释的行外的其它行(该字段的典型用法是用于设置断点。大多数调试器不允许在活跃行外设置断点,因为非活跃行是不可达的)。 |
nups | 该字段是该函数的上值的个数 |
nparams | 该字段是该函数的参数个数 |
isvararg | 该字段表明该函数是否为可变长函数 |
返回值:
如果传递的是一个函数或是一个合理的数值(小于等于栈层级),则会返回对应函数的信息表。如果超出的栈层级,则返回 nil
值得注意:
如果假设 foo 是一个 C 函数,Lua 语言没有多少关于该函数的信息。只有字段 what、name、namewhat、nups 和 func 是有意义的。
举两个例子:
输出一个函数的信息
lua
function foo(a, b, ...)
print("江澎涌")
end
local info = debug.getinfo(foo)
for k, v in pairs(info) do
print(k, "---", v)
end
使用数值调用
lua
foo1 = function(...)
local table = debug.getinfo(1)
for k, v in pairs(table) do
print(k, "---", v)
end
end
foo1()
上面例子中调用栈的层次如下:
2、traceback(thread, message, level)
返回调用栈信息
参数:
- thread: 表示在 thread 中获取相应的信息
- message:该参数没有限定为字符串,可以是任意的类型。如果为字符串或 nil ,则会返回调用栈的描述字符串,并且在最开始的地方拼接该 message (如果为 nil ,则不拼接)。如果为其他类型,则直接放回该值。
- level :调用层级,0 表示
traceback
函数,1 表示调用traceback
函数的函数,2 表示调用traceback
函数的函数的函数 ......
返回值:
如果 message 为字符串或 nil ,则会返回调用栈的描述字符串,并且在最开始的地方拼接该 message (如果为 nil ,则不拼接)。如果为其他类型,则直接返回该值。
举个例子
lua
print("没有参数")
local function foo1()
print(debug.traceback())
end
foo1()
--> 没有参数
--> stack traceback:
--> ...Lua/lua_study_2022/18 调试库/自省机制-getInfo.lua:58: in local 'foo1'
--> ...Lua/lua_study_2022/18 调试库/自省机制-getInfo.lua:60: in main chunk
--> [C]: in ?
print("携带 message(字符串)")
local function foo2()
print(debug.traceback("track back message."))
end
foo2()
--> 携带 message(字符串)
--> track back message.
--> stack traceback:
--> ...Lua/lua_study_2022/18 调试库/自省机制-getInfo.lua:64: in local 'foo2'
--> ...Lua/lua_study_2022/18 调试库/自省机制-getInfo.lua:66: in main chunk
--> [C]: in ?
print("携带 message(非字符串)")
local function foo3()
print(debug.traceback({}))
end
foo3()
--> 携带 message(非字符串)
--> table: 0x600000cf0d80
print("携带 message(非字符串)且携带 level ")
local function foo4()
--print(debug.traceback("track back message.", 0))
--print(debug.traceback("track back message.", 1))
print(debug.traceback("track back message.", 2))
--print(debug.traceback(nil, 2))
end
foo4()
--> 携带 message(非字符串)且携带 level
--> track back message.
--> stack traceback:
--> ...Lua/lua_study_2022/18 调试库/自省机制-getInfo.lua:81: in main chunk
--> [C]: in ?
3、getlocal(thread, f, var)
通过 f
(可以是函数也可以栈层次)和 var
指定的索引,返回对应的变量的名称和值。此处的变量包括:局部变量,参数和临时变量。
参数
- thread: 表示在 thread 中获取相应的信息
- f:该值可以是函数也可以是栈层次。如果是函数,则该函数只会参数的名称,不会有其他的局部变量和临时变量(因为这个时候函数还未运行,所以对于运行时来说,都是不知道的)。如果是栈层级,则可以返回局部变量,参数和临时变量(但仅限于活跃的变量)。
- var :参数或局部变量的索引。如果为正数,则按照变量的申明顺序(包括参数),从 1 开始往后访问所有的变量。如果为负数,则范围的是可变参数,从 -1 开始访问( -1 表示第一个)。如果索引没有对应的值,则返回
nil
。
值得注意的是如果栈层级传递超出可用范围,则会导致抛出异常,可以使用 getinfo
先检测栈层级是否正确( getinfo
对非法的栈层级只会返回 nil ,不会有异常)。
返回值:
返回变量名和变量值。
如果该值没有已知的名称(例如 for 循环中的临时变量),则会是以 "( " 括号开头的变量来代替名称
举个例子:
用栈层级进行调用
lua
local outerParam = "外部局部变量"
globalParam = "全局变量"
local function foo(funParamA, funParamB, ...)
local localParamX
do
local localParamC = funParamA - funParamB
end
local localParamA = 1
print("------------------------------------")
print("遍历变量:")
-- for 循环只是为了临时模拟临时变量,所以一次循环就退出
for i = 1, 2 do
while true do
local name, value = debug.getlocal(1, localParamA)
if not name then
break
end
print(name, "---", value)
localParamA = localParamA + 1
end
break
end
print("------------------------------------")
print("遍历可变参数:")
localParamA = -1
while true do
local name, value = debug.getlocal(1, localParamA)
if not name then
break
end
print(name, "---", value)
localParamA = localParamA - 1
end
end
foo(1, 200, "jiang", "pengyong")
--> ------------------------------------
--> 遍历变量:
--> funParamA --- 1
--> funParamB --- 200
--> localParamX --- nil
--> localParamA --- 4
--> (for state) --- 1
--> (for state) --- 1
--> (for state) --- 1
--> i --- 1
--> ------------------------------------
--> 遍历可变参数:
--> (vararg) --- jiang
--> (vararg) --- pengyong
对函数进行调用
lua
local function foo(funParamA, funParamB, ...)
local localParamX = "江澎涌"
end
for i = 1, math.huge do
local name, value = debug.getlocal(foo, i)
if not name then
break
end
print(name, "---", value)
end
-- 不会有局部变量和临时变量,因为此时函数还未运行
--> 遍历局部变量(函数):
--> funParamA --- nil
--> funParamB --- nil
4、debug.setlocal(thread, level, var, value)
将值 value
设置给位于栈层级为 level
的函数中索引为 var
的局部变量。
参数:
- thread: 表示在 thread 中获取相应的信息
- level :栈层级,和 getlocal 的
f
是一样的(只是不能传递函数,必须是数值) - var:变量索引
- value:需要设置的值
返回值:
- 如果没有具有给定索引(var)的局部变量,该函数将返回
nil
- 如果
level
超出合理的栈层级,则会抛出异常。 可以调用getinfo
来检查级别是否有效 - 如果一切正常则返回局部变量的名称
举个例子:
lua
function showLocalParam()
for i = 1, math.huge do
local name, value = debug.getlocal(2, i)
if not name then
break
end
print(name, "---", value)
end
end
local name = "jiang pengyong"
local age = 29
do
local name = "江澎涌"
print("-------------------------------------")
print("设置前:")
showLocalParam()
print("-------------------------------------")
-- 修改的是外部的 name ,而非内部的 name
print("debug.setlocal",debug.setlocal(1, 1, "jiang"))
print("-------------------------------------")
print("设置后:")
showLocalParam()
end
--> 设置前:
--> name --- jiang pengyong
--> age --- 29
--> name --- 江澎涌
--> -------------------------------------
--> debug.setlocal name
--> -------------------------------------
--> 设置后:
--> name --- jiang
--> age --- 29
--> name --- 江澎涌
5、getupvalue(f, up)
返回函数 f
索引为 up
的上值的名称和值。如果给定索引没有上值,则该函数返回 nil
。
如果无法知道变量的名称,则会返回以 "(" 开头的变量名。(例如临时变量)
举个例子:
通过下面代码可以遍历闭包的所有上值
要区分好上值和全局变量的概念,我们平常所说的全局变量其实是存储在上值的 _ENV 中,编译器会做一层转换(这个在 "环境(_G 和 _ENV)" 一章中已有详细分享);而上值指的是函数所能访问到的非局部变量(例如下面代码段的 age、heavy 变量)。如果函数中不使用该变量,则该变量就不会被算作是上值(例如 heavy),如果函数中没有使用到任何的全局变量,则 _ENV 都会不存在(注意这里连函数 print 都不能为全局的,否则达不到该条件,具体可以看第二个例子)。
lua
name = "江澎涌"
local age = 29
function foo()
local heavy = 120
return function()
print(age)
-- 如果没有调用 heavy ,则 heavy 就不是该闭包的上值
--print(heavy)
local func = debug.getinfo(1, "f").func
for i = 1, math.huge do
local n, v = debug.getupvalue(func, i)
if not n then
break
end
print(n, "--------", v)
end
end
end
foo()()
--> 29
--> _ENV -------- table: 0x600001470200
--> age -------- 29
getupvalue 获取上值(不使用全局变量,连 _ENV 都会没有)
这里的 print、getinfo、getupvalue、huge 都需要转为 loacl ,才能避免编译过程中编译器自动为全局变量加上 _ENV ,导致上值引入了 _ENV 变量。
lua
name = "江澎涌"
local print = print
local getinfo = debug.getinfo
local getupvalue = debug.getupvalue
local huge = math.huge
local age = 29
function foo()
local heavy = 120
return function()
print(age)
local func = getinfo(1, "f").func
for i = 1, huge do
local n, v = getupvalue(func, i)
if not n then
break
end
print(n, "--------", v)
end
end
end
foo()()
-- 可以看到 _ENV 变量不见了,因为没有引用到任何的 "全局变量"
--> 29
--> print -------- function: 0x106ac4ac0
--> age -------- 29
--> getinfo -------- function: 0x106ac5e30
--> huge -------- inf
--> getupvalue -------- function: 0x106ac6500
6、debug.setupvalue(f, up, value)
将值 value
设置给给函数 f
的索引为 up
的上值。
如果给定索引没有上值,则该函数返回 nil
。 否则,它返回上值的名称。
lua
name = "江澎涌"
local age = 29
local function foo()
local heavy = 120
function showinfo(func)
for i = 1, math.huge do
local n, v = debug.getupvalue(func, i)
if not n then
break
end
print(n, "--------", v)
end
end
return function()
print(age)
local func = debug.getinfo(1, "f").func
print("设置上值前:")
showinfo(func)
-- 这里的 2 刚好就是指 age 变量
debug.setupvalue(func, 2, 116)
print("设置上值后:")
showinfo(func)
end
end
foo()()
--> 29
--> 设置上值前:
--> _ENV -------- table: 0x600000f38040
--> age -------- 29
--> 设置上值后:
--> _ENV -------- table: 0x600000f38040
--> age -------- 116
7、debug.debug()
该函数可以让用户进入交互模式,运行用户输入的每个字符串。
可以使用简单的命令和其他调试工具,可以检查全局和局部变量、更改它们的值、计算表达式等。
使用 cont
结束此函数
值得注意,debug.debug
命令并未按词法嵌套在任何函数中,因此无法直接访问局部变量。
8、debug.setmetatable(value, table)
将table
(可以是nil
)设置为 value
作为元表,会将 value 返回。
和 setmetatable
的区别在于,debug.setmetatable
绕开了元表原有的检测机制。例如:无论 value 的元表是否设置了 __metatable
,对元表进行保护,不被外部篡改,都能进行修改。
例子在下面小节结合 debug.getmetatable
给出
9、debug.getmetatable(value)
返回给定 value 的元表,如果没有元表则返回 nil 。
和 getmetatable
的区别在于,debug.getmetatable
也是绕开了元表的检测机制。例如:无论 value 的元表是否设置了 __metatable
,都能获取到真正的元表。
举个例子
lua
local table = {}
local mt = { __metatable = "protect metatable" }
-- 给 table 设置一个带有 __metatable 的元表,用于保护元表
print(setmetatable(table, mt))
-- 如果使用 getmetatable 则只能获取到 __metatable 的值
print(getmetatable(table))
-- 如果使用 debug.getmetatable 则可以获取到 table 的元表
print(debug.getmetatable(table), mt)
-- setmetatable 设置会报错,因为 table 的元表被保护者,会抛 "cannot change a protected metatable" 异常
--setmetatable(table, nil)
-- debug.setmetatable 则可以正常设置
print(debug.setmetatable(table, nil))
-- 通过打印,验证设置有效
print(getmetatable(table))
10、debug.setuservalue(udata, value, n)
将给定的 value 设置为给定的 udata 相关联的第 n 个用户值。
但 udata 必须是一个完整的 userdata。
如果设置成功,则会返回 udata;如果 userdata 没有该值,则返回 nil。
11、debug.getuservalue(u, n)
获取与 u (userdata 类型)相关联的第 n 个用户值,会返回该值以及一个布尔值。
如果 userdata 没有该值,布尔值为 false。
12、debug.getregistry()
返回注册表。注册表是一个特殊的 Lua 表,可以被用于存储全局数据或跟踪引用。但正常的使用不建议使用它来作为全局变量的储存点。
lua
registry = debug.getregistry()
registry.name = "江澎涌"
for i, v in pairs(registry) do
print(i, "-----", v)
end
--> 1 ----- thread: 0x7f88ed008e08
--> 2 ----- table: 0x60000293c200
--> FILE* ----- table: 0x60000293c540
--> name ----- 江澎涌
--> _IO_output ----- file (0x7ff85810fc98)
--> _CLIBS ----- table: 0x60000293c300
--> _IO_input ----- file (0x7ff85810fc00)
--> _LOADED ----- table: 0x60000293c280
--> _PRELOAD ----- table: 0x60000293c440
13、debug.upvalueid(f, n)
从给定函数返回编号为 n
的上值的唯一标识符(作为轻型用户数据)。
这些唯一标识符允许程序检查不同的闭包是否共享 upvalues。 共享一个上值(即访问同一个外部局部变量)的 Lua 闭包将为这些上值索引返回相同的 id。
14、debug.upvaluejoin(f1, n1, f2, n2)
使 Lua 闭包 f1 的第 n1 个上值引用 Lua 闭包 f2 的第 n2 个上值。
三、传入协程参数
在第二节中,可以看到大多函数的第一个函数是 thread ,这意味着可以传入一个协程。
以 trackback 为例,对一个协程进行栈的获取
lua
co = coroutine.create(function()
local x = 10
coroutine.yield(x)
error("coroutine error............")
end)
print("运行返回", coroutine.resume(co))
print("打应栈", debug.traceback(co))
--> 运行返回 true 10
--> 打应栈 stack traceback:
--> [C]: in function 'coroutine.yield'
--> .../Lua/lua_study_2022/18 调试库/自省机制-协程.lua:9: in function <.../Lua/lua_study_2022/18 调试库/自省机制-协程.lua:7>
print("运行返回", coroutine.resume(co))
print("打应栈", debug.traceback(co))
--> 运行返回 false .../Lua/lua_study_2022/18 调试库/自省机制-协程.lua:10: coroutine error............
--> 打应栈 stack traceback:
--> [C]: in function 'error'
--> .../Lua/lua_study_2022/18 调试库/自省机制-协程.lua:10: in function <.../Lua/lua_study_2022/18 调试库/自省机制-协程.lua:7>
print("获取局部变量", debug.getlocal(co, 1, 1)) --> 获取局部变量 x 10
四、钩子函数
1、debug.sethook(thread, hook, mask, count)
设置钩子
参数:
- thread: 表示在 thread 中设置相应的钩子
- hook :钩子被触发时,便会表用该函数。函数的第一个参数是事件类型,包括
call
(也包括尾调用)、return
、line
、count
,对于line
事件则会有第二个参数,表示行号。 - mask:字符串掩码,可以是以下的任意字符组合
掩码 | 描述 |
---|---|
c | 每次 Lua 调用函数时都会调用钩子 |
r | 每次 Lua 从函数返回时调用钩子 |
l | 每次 Lua 进入一个新的代码行时调用这个钩子 |
对于检测频率,只需要填入 count 参数就行 |
- count:指定以什么频率获取 count 事件
关闭钩子:
当不带参数调用时,debug.sethook
关闭钩子
钩子的使用:
在一个钩子里面,可以用 level 为 2 调用 getinfo
来获取更多关于正在运行的函数的信息(0 级是 getinfo
函数,level 1 是钩子函数)
举个例子:
lua
debug.sethook(print, "l")
print("江澎涌")
--> line 15
--> 江澎涌
print("小朋友")
--> line 16
--> 小朋友
debug.sethook()
--> line 17
2、debug.gethook(thread)
返回 thread 的当前挂钩设置,
会返回三个值:当前挂钩函数、当前挂钩掩码和当前挂钩计数(这些值都是 debug.sethook
函数设置)。
五、写在最后
Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 "江澎涌",更多优质文章会第一时间分享与你。