Lua 调试库( debug )

一、调试库作用

使用调试库可以获取 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(也包括尾调用)、returnlinecount,对于 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吧,码字不易,请多多支持)

如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀

公众号搜索 "江澎涌",更多优质文章会第一时间分享与你。

相关推荐
jmlinux17 分钟前
环形缓冲区(Ring Buffer)在STM32 HAL库中的应用:防止按键丢失
c语言·stm32·单片机·嵌入式硬件
s_little_monster25 分钟前
【QT】QT入门
数据库·c++·经验分享·笔记·qt·学习·mfc
Yingye Zhu(HPXXZYY)32 分钟前
洛谷 P11045 [蓝桥杯 2024 省 Java B] 最优分组
c++·蓝桥杯
三玖诶41 分钟前
第一弹:C++ 的基本知识概述
开发语言·c++
木向2 小时前
leetcode42:接雨水
开发语言·c++·算法·leetcode
TU^2 小时前
C语言习题~day16
c语言·前端·算法
DdddJMs__1352 小时前
C语言 | Leetcode C语言题解之第461题汉明距离
c语言·leetcode·题解
sukalot2 小时前
windows C++-创建基于代理的应用程序(下)
c++
labuladuo5202 小时前
AtCoder Beginner Contest 372 F题(dp)
c++·算法·动态规划