近期研究了一下Lua语言在解析时的一些细节,如果在C程序中执行lua脚本的话, 那么变量的作用域是非常值得关注的,这里记录一下在分析过程中得到的一些结论。(本文的描述针对的是lua-5.1.5这个版本的代码)
考察下面的两段代码:
scope.lua
1 b = 700 -- GT['b'] = 700 2 local a = 9 -- 设置在栈上 3 4 function p1() -- GT['p1'] = Closure B 5 m = 90 -- GT['m'] = 90 6 local n = 8 -- 设置在栈上 7 print(a) -- 数据来自upval 8 print(b) -- 数据来自GT 9 end 10 11 function p2() -- GT['p2'] = Closure C 12 print(m) -- 数据来自GT 13 print(n) -- nil 14 end
srv.c
1 ... 2 luaL_loadfile(L, "scope.lua"); 3 lua_pcall(L, 0, 0, 0); // 执行闭包A的字节码 4 ... 5 lua_getglobal(L, "p1"); 6 lua_pcall(L, 0, 0, 0); // 执行闭包B的字节码 7 8 lua_getglobal(L, "p2"); 9 lua_pcall(L, 0, 0, 0); // 执行闭包C的字节码 10 ...
scope.lua脚本中会生成三个闭包A、B和C,其中闭包A是由scope.lua脚本加载(luaL_loadfile)之后生成的,luaL_loadfile最终会调用f_parse来解析脚本并生成闭包A,并且闭包A的环境table会指定为L
的global table。
调用闭包A
接下来在srv.c中第3行将会执行闭包A对应的字节码,操作包括:
- 将变量b设置在闭包A的环境Table中
- 将变量a设置在栈上
- 生成闭包B,赋值给变量p1,同时设置在闭包A的环境Table中,指定闭包B的环境Table就等于闭包A的环境Table。生成的闭包B存在一个upval,指向上一层的局部变量a。
- 生成闭包C,赋值给变量p2,同时设置在闭包A的环境Table中,同理闭包C的环境Table也等于闭包A的环境Table。
可以看到变量b、p1和p2都会保存在闭包A的环境Table中,也就是L
的global table中。
调用闭包B
在srv.c的第5行执行之后,会将闭包B设置在栈顶,接下来调用lua_pcall便会执行闭包B对应的字节码, 操作如下:
- 将变量m设置在闭包B的环境Table中
- 将变量n设置在栈上
- 从闭包B的upval中找到变量a的值并设置在栈上,调用print
- 从闭包B的环境Table中找到变量b的值并设置在栈上,调用print
前面讲过,闭包B的环境Table和闭包A的环境Table是一致的并且都是L
的global table,因此可以得到下面的输出结果:
9 700
调用闭包C
同样的,srv.c执行到第8行和第9行的时候会执行闭包C的字节码,变量m是从闭包C的环境Table也就是L
的global table中获取,而变量n是闭包B的局部变量,没有设置在环境Table中,也不存在于闭包C的upval中,因此结果会为空,得到的结果如下:
90 nil
总结
现在有很多用C语言实现的服务器程序都会嵌入Lua脚本来提高开发效率,并且通过在一个Lua虚拟机中创建多个Lua线程的手段来对每个请求的处理进行区分,因此在编写Lua脚本的时候要很清楚每个变量的作用域范围,否则可能会出现数据不一致的情况,某些变量可能是被一个Lua虚拟机中的所有Lua线程共享,而某些变量只会存在于一个Lua线程独立的数据栈中。
此外,程序中很有可能还会调用一些API来更改Lua线程的global table或环境table,因此更需要特别关注。
参考
- http://www.codingnow.com/temp/readinglua.pdf – Lua 源码欣赏 – 云风工作室
- https://github.com/openresty/lua-nginx-module – 在Nginx中嵌入Lua脚本
- https://github.com/portl4t/ts-lua – 在Apache Traffic Server中嵌入Lua脚本
- http://www.lua.org/