0%

《lua 程序设计》读书笔记(12):反射

反射是程序用来检查和修改自身某些部分的能力。Lua 语言本身支持一部分反射能力,而调试库也提供了相关能力。虽然调试库提供的并不是 Lua 语言的调试器,但是其提供了编写我们自己调试器所需要的不同层次的所有底层机制。调试库与其他库不同,必须被慎重使用,因为某些功能性能不高,而且调试库会打破语言的一些固有限制。

自身机制

调试库主要的自身函数是 getinfo,该函数的第一个参数可以是一个函数或者一个栈层次:

  • 当以函数为参数时,会返回与该函数有关的一些数据的表
  • 当以数字 n 作为参数时,可能得到有关相应栈层次上活跃函数的数据。调用 getinfo 的函数层次为 1,之后依次递增。当 n 大于栈中活跃函数的数量,那么函数 debug.getinfo 返回 nil

函数 getinfo 的效率不高,为了实现更好的性能,函数 getinfo 有一个可选的第二个参数,该参数用于指定希望获取哪些信息。如下实现一个简易版的 traceback:

1
2
3
4
5
6
7
8
9
10
11
12
13
    for level = 1, math.huge do
local info = debug.getinfo(level, "Sl")
if not info then
break
end

if info.what == "C" then
print(string.format("%d\t C function", level))
else
print(string.format("%d\t[%s]:%d", level, info.short_src, info.currentline))
end
end
end

实际上,debug 库本身提供了 traceback 用于返回包含 栈回溯 的字符串。

可以通过 debug.getlocal 来检查任意活跃函数的局部变量。该函数有两个参数:一个是要查询函数的栈层次,另一个是变量索引。返回变量名和变量的当前值。Lua 按照局部变量在函数中出现的顺序对其进行编号。还可以通过函数 debug.setlocal 改变局部变量的值。

debug.getupvalue 允许我们访问一个被 Lua 函数所使用的非局部变量。getupvalue 的第一个参数是一个函数(准确说是一个闭包),第二个参数是变量索引,Lua 按照函数引用非局部变量的顺序对他们编号。debug.setupvalue 用于更新非局部变量的值。

如下函数展示了如何通过变量名访问一个函数中变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function getvarvalue(name, level, isenv)
local value
local found = false

level = (level or 1) + 1

for i = 1, math.huge do
local n, v = debug.getlocal(level, i)
if not n then
break
end

if n == name then
value = v
found = true
end
end

if found then
return "local", value
end

local func = debug.getinfo(level, "f").func
for i = 1, math.huge do
local n, v = debug.getupvalue(func, i)
if not n then
break
end

if n == name then
return "upvalue", v
end
end

if isenv then
return "noenv"
end

local _, env = getvarvalue("_ENV", level, true)
if env then
return "global", env[name]
else
return "noenv"
end
end

local a = 4
print(getvarvalue("a"))

b = "test"
print(getvarvalue("b"))

调试库所有的自身函数都能接受一个可选的协程作为第一个参数,这样就能从外部来检查这个协程。

1
2
3
4
5
6
7
8
9
10
11
12
> co = coroutine.create(function()
>> local x = 10
>> coroutine.yield()
>> error("some error")
>> end)

> coroutine.resume(co)
true
> print(debug.traceback(co))
stack traceback:
[C]: in function 'coroutine.yield'
stdin:3: in function <stdin:1>

由于协程和主程序运行在不同的栈上,所以上述示例栈回溯没有跟踪到对 resume 的调用。当协程引发错误时,并不会进行栈展开,这就意味着可以在错误发生后检查错误。

1
2
3
4
5
6
7
8
9
10
> print(coroutine.resume(co))
false stdin:4: some error

> print(debug.traceback(co))
stack traceback:
[C]: in function 'error'
stdin:4: in function <stdin:1>

> print(debug.getlocal(co, 1, 1))
x 10

钩子

调试库中的钩子机制允许用户注册一个钩子函数,这个钩子会在程序运行中某个特定事件发生时被调用。有四种事件能够触发一个钩子:

  • 每当调用一个函数时产生的 call 事件
  • 每当函数返回时产生的 return 事件
  • 每当开始执行一行新的代码时产生的 line 事件
  • 执行完指定数量后产生的 count 事件

使用 debug.sethook 可以注册一个钩子。如果要关闭钩子,只需要不带任何参数地调用 sethook 即可。

1
2
3
4
5
6
7
8
function exec()
print("line 1")
print("line 2")
print("line 3")
end

debug.sethook(exec, "l")
exec()

debug.debug 可以提供一个能够执行任意 Lua 语言命令的提示符,其等价于如下代码:

1
2
3
4
5
6
7
8
9
10
11
function debug1()
while true do
io.write("debug> ")
local line = io.read()
if line == "cont" then
break
end

assert(load(line))()
end
end

调优

除了调试,反射的另外一个常见用法是用于调优,即程序使用资源的行为分析。对于时间相关的调优,最好使用 C 接口,因为钩子调用函数开销太大从而可能导致测试效果无效。对于计数性质的调优,Lua 也能实现。

如下用来统计函数被调用的次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
local Counters = {}
local Names = {}

local function hook()
local f = debug.getinfo(2, "f").func
local count = Counters[f]
if count == nil then
Counters[f] = 1
Names[f] = debug.getinfo(2, "Sn")
else
Counters[f] = count + 1
end
end


function getname(func)
local n = Names[func]
if n.what == "C" then
return n.name
end

local lc = string.format("[%s]:%d", n.short_src, n.linedefined)
if n.what ~= 'main' and n.namewhat ~= "" then
return string.format("%s (%s)", lc, n.name)
else
return lc
end
end

function show()
for func, count in pairs(Counters) do
print(getname(func), count)
end
end

local f = assert(loadfile(arg[1]))
debug.sethook(hook, "c")
f()
debug.sethook()
show()

沙盒

利用函数 load 在受限的环境中运行 Lua 代码是非常简单的。但是我们仍然可能被消耗大量 CPU 时间或内存的脚本进行拒绝服攻击。反射以调试钩子的形式,提供了一种避免攻击的方式。

可以使用 count 事件来限制一段代码能够执行的指令数目。如下代码使得 Lua 每执行 100 条指令就调用一次 hook 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local debug = require "debug"

local steplimit = 1000
local count = 0

local function step()
count = count + 1
if count > steplimit then
error("scripts use too much cpu")
end
end

local f = assert(loadfile(arg[1], "t", {}))
debug.sethook(step, "", 100)
f()

除了限制对 CPU 的消耗,还要限制所加载代码段大小、脚本运行所消耗的内存大小。如下函数用来检查内存的占用:

1
2
3
4
5
6
7
local memlimit = 1000

local function checkmem()
if collectgarbage("count") > memlimit then
error("script uses too much memory")
end
end

有时候我们还需要检查对某些库函数的调用是否合法,这里我们可以借助 call 钩子实现。

使用协程实现多线程

这里在讨论一下如何通过协程来实现多线程,该话题与调试、反射关系不大。

协程能够实现一种协作式多线程,每个协程都等价于一个线程。一对 yield-resume 可以将执行权在不同协程之间切换。但是和普通多线程不同,协程是非抢占的。当一个协程在运行时,是无法从外部停止它的,只有当协程显式要求时它才会挂起执行。由于程序中协程间的同步都是显式的,所以无需为同步问题过多考虑,只要确保一个协程只在它的临界区之外调用 yield 即可。但是对于非抢占式多协程来说,只要一个协程调用了阻塞操作,整个程序在该操作完成前都会阻塞,所以很多程序员也不把协程看做 传统多线程 的一种实现原因。

如下使用协程来达到类似多线程的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function receive(connection)
connection:settimeout(0)
local s, status, partial = connection:receive(2^10)
if status == "timeout" then
coroutine.yield(connection)
end
return s or partial, status
end

function download(host, file)
local c = assert(socket.connect(host, 80))
local count = 0
local request = string.format(
"GET %s HTTP/1.0\r\nhost: %s\r\n\r\n", file, host
)
c:send(request)

while true do
local s, status = receive(c)
count = count + #s
if status == "closed" then
break
end
end

c:close()
print(file, count)
end

tasks = {}

function get(host, file)
local co = coroutine.wrap(function ()
download(host, file)
end)

table.insert(tasks, co)
end

function dispatch()
local i = 1
local timeout = {}

while true do
if tasks[i] == nil then
if tasks[1] == nil then
break
end
i = 1
timedout = {}
end

local res = task[i]()
if not res then
table.remove(tasks, i)
else
i = i + 1
timeout[#timeout + 1] = res
if #timeout == #tasks then
socket.select(timedout)
end
end
end
end

get("a.com", "1")
get("a.com", "2")
get("a.com", "3")