转载

怎样在运行时插入运行一段 Lua 代码

最近想给 skynet 加一个在线调试器,方便调试 Lua 编写的服务。

Lua 本身没有提供现成的调试器,但有功能完备的 debug api 。通常、我们可以在代码中插入 debug.debug() 就可以进入一个交互环境,输入任何 Lua 指令。当然,你也可以在 debug hook 里调用它。

但这种交互方式有一个缺点:lua 直接用 load 翻译输入的文本,转译为一个 lua 函数并运行。这注定了这个输入的代码中不能直接访问到上下文的局部变量和 upvalue 。

如果想读写上下文中的局部变量或 upvalue ,还得使用 debug.getlocal 等函数。这无疑是相当麻烦的。

有没有办法实现一个增强版的 dostring ,让运行的代码拥有和调用者相同的上下文呢?

可以,但需要一点技巧。

我们不要直接加载要运行的代码,而是给它构造一个类似的环境。比如当前有两个 local 变量 a 和 b 的话,就在代码字符串前加上 local a,b 。然后在运行它之前,把值写进入。

既然我们知道注入了哪些变量,就可以在运行完毕后,读出这些变量再设回当前环境中即可。

对于当前的 upvalue ,要更容易一些。

因为,Lua 5.2 之后提供了 debug.upvaluejoin 可以把当前的 upvalue 关联到你要运行的函数上,这样连事后更新都省了。

处理 ... 这种可变参数要麻烦一些。你需要自己小心的一个个读出来,再传给你要插入的函数。

这套方案说起来简单,实现起来还是比较绕的。ok ,我实现了一份供参考。使用这个版本的 run ,可以运行一个字符串,它拥有和调用者完全相同的环境,就好像代码被嵌在当前位置一样。注意,如果当前环境没有引用某个 upvalue ,即使它可见,你插入的代码也不可能看见。如果想获取它,可以传递恰当的 level ,切到合适的层次上就可以访问了。

有了这个函数,我们可以这样使用:

local uv = 2 function f(...)  local a,b = 1,uv  print("_ENV ===>", _ENV)  print("a,b ====>", a,b)  run[[   print "=== inject code ==="   print("/t_ENV ===>", _ENV)   print("/ta,b,uv ===>", a,b,uv)   print("/t... ", ...)   a,b = b,a   uv = 3   print "==== level ==="  ]]  print("a,b ====>", a,b) end f("Hello","world") print("uv=",uv) 

运行它可以得到这样的输出:

_ENV ===>       table: 0000000000266de0 a,b ====>       1       2 === inject code === _ENV ===>       table: 0000000000266de0 a,b,uv ===>     1       2       2 ...     Hello   world ==== level === a,b ====>       2       1 uv=     3

你可以看到,在 f 函数中插入运行了一段代码,它可以访问到 f 函数可见的 a,b 两个 local 变量, 以及 f 引用的 upvalue uv 。并可以改写它们。可变参数也可以用 ... 正确访问到。

run 的实现我放在 gist 上了 。

如果在 debug hook 里使用 run ,就需要调用时传入 level (通常是 1 )。这份实现性能并不高(如果需要高性能,可以用 C 重新实现一遍),推荐用在交互调试器上,而不要用于生产代码中。

正文到此结束
Loading...