解析Lua中的全域性環境、包、模組組織結構
1. 環境
Lua將環境table儲存在一個全域性變數_G中,可以對其訪問和設定。有時我們想操作一個全域性變數,而它的名稱卻儲存在另一個變數中,或者需要通過執行時的計算才能得到,可以通過value = _G[varname]來獲得動態名字的全域性變數。
關於“環境”的一大問題是它是全域性的,任何對它的修改都會影響程式的所有部分。Lua 5允許每個函式擁有一個子集的環境來查詢全域性變數,可以通過setfenv來改變一個函式的環境,第一個引數若是1則表示當前函式,2則表示呼叫當前函式的函式(依次類推),第二個引數是一個新的環境table。
a = 1 setfenv(1, {}) print(a) -- 會報錯,print是一個nil。這是因為一旦改變環境,所有的全域性訪問都會使用新的table
為了避免上述問題,可以使用setfenv(1, {_G = _G})將原來的環境儲存起來,然後用_G.print來引用。另一種組裝新環境的方法是使用繼承,下面的程式碼新環境從源環境中繼承了print和a,任何賦值都發生在新的table中。
a = 1
local newgt = {}
setmetatable(newgt, {__index = _G})
setfenv(1, newgt)
print(a)
2. 模組與包
2.1 呼叫模組
要呼叫模組mod中的foo方法,可以用require函式來載入,如:
require "mod" mod.foo() -- 或者 local m = require "mod" m.foo()
require函式的行為: (關於require使用的路徑查詢策略不贅述)
在package.loaded這個table中檢查模組是否已載入
=> 已載入,就返回相應的值(可見一個模組只會載入一次)
=> 未載入,就試著在package.preload中查詢傳入的模組名
===> 找到一個函式,就以該函式作為模組的載入器
===> 找不到,則嘗試從Lua檔案或C程式庫中載入模組
=====> 找到Lua檔案,通過loadfile來載入檔案
=====> 找到C程式庫,通過loadlib來載入檔案
2.2 使用環境
下面的程式碼說明了如何用環境來建立一個複數(complex)模組:
-- 模組設定
local modname = "complex"
local M = {}
_G[modname] = M
package.loaded[modname] = M
-- 宣告模組從外界所需的所有東西
local _G = _G -- 保留舊環境的引用,使用時需要像_G.print這樣用
local io = io
-- 執行這句之後環境就變了
setfenv(1, M)
function new(r, i) return {r=r, i=i} end
function add(c1, c2)
return new(c1.r + c2.r, c1.i + c2.i)
end
這樣宣告函式add時,就成為了complex.add,呼叫同一模組的其他函式也不需要加字首。
2.3 module函式
Lua 5.1提供了一個新函式module,囊括了上面一系列定義環境的功能。在開始編寫一個模組時,可以直接用module("modname", package.seeall)來取代前面的設定程式碼。在一個模組檔案開頭有這句呼叫後,後續所有程式碼都不需要限定模組名和外部名字,同樣也不需要返回模組table了。
2.4 子模組與包
Lua支援具有層級的模組名,用一個點來分隔名稱中的層級。例如一個模組名為mod.sub,就是mod的一個子模組。一個包(package)就是一個完整的模組樹,它是Lua中髮型的單位。注意,當搜尋一個子模組檔案時,require會把點號當做目錄分隔符來搜尋,也就是說呼叫require "a.b"會嘗試開啟./a/b.lua,/usr/local/lua/a/b.lua,/usr/local/lua/a/b/init.lua。通過這種載入策略,可以將包的所有模組組織到一個目錄中。
2.5 以自定義方式載入 lua 模組
從 Lua 5.1 以後,Lua 有了標準的模組管理庫。所以所有的模組載入都是通過 require 來完成。 require 的設計是頗具擴充套件性的,它會從若干個定義好的 loader 中逐個嘗試載入新的模組。系統庫中提供了四個 loader ,分別實現已載入模組,Lua 模組,和 C 擴充套件模組(用了兩個 loader 來實現 C 擴充套件模組的載入)。這些 loader 以 CFunction 的形式放在 require 的環境中的一個 table 裡。
如果我們想改變 lua 模組的載入形式,只需要替換或增加一個新的 loader 就可以了。
要做的只需要模仿 loadlib.c 中的 loader_Lua 函式做一個自己的實現,比如在我們的專案中,就允許從自定義格式資料包中,載入一個被加密過的 Lua 程式碼檔案。然後寫幾行 C 程式碼,獲得 require 的環境(使用 lua_getfenv ),然後取出其中 "loaders" 這個 table ,把新的自定義 loader 插入到 index 2 的地方。
具體的程式碼就不詳述了,仔細閱讀一下 ll_require 的實現(在 loadlib.c 中)就很容易明白。我們的整個工作從分析到實現沒有超過兩個小時,這真是得益於 Lua 良好的設計啊 :D 甚至如果你想從一個網路連線的資料流中載入 Lua 模組,或是通過 http/ftp 協議下載,也是行的通的吧。