近期研究了一下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/