Lua 模組與包

模組類似於一個封裝庫,從 Lua 5.1 開始,Lua 加入了標準的模組管理機制,可以把一些公用的代碼放在一個檔裏,以 API 介面的形式在其他地方調用,有利於代碼的重用和降低代碼耦合度。

Lua 的模組是由變數、函數等已知元素組成的 table,因此創建一個模組很簡單,就是創建一個 table,然後把需要導出的常量、函數放入其中,最後返回這個 table 就行。以下為創建自定義模組 module.lua,檔代碼格式如下:

-- 檔案名為 module.lua
-- 定義一個名為 module 的模組
module = {}
 
-- 定義一個常量
module.constant = "這是一個常量"
 
-- 定義一個函數
function module.func1()
    io.write("這是一個公有函數!\n")
end
 
local function func2()
    print("這是一個私有函數!")
end
 
function module.func3()
    func2()
end
 
return module

由上可知,模組的結構就是一個 table 的結構,因此可以像操作調用 table 裏的元素那樣來操作調用模組裏的常量或函數。

上面的 func2 聲明為程式塊的局部變數,即表示一個私有函數,因此是不能從外部訪問模組裏的這個私有函數,必須通過模組裏的公有函數來調用.


require 函數

Lua提供了一個名為require的函數用來加載模組。要加載一個模組,只需要簡單地調用就可以了。例如:

require("<模組名>")

或者

require "<模組名>"

執行 require 後會返回一個由模組常量或函數組成的 table,並且還會定義一個包含該 table 的全局變數。

test_module.lua 檔

-- test_module.lua 檔
-- module 模組為上文提到到 module.lua
require("module")
 
print(module.constant)
 
module.func3()

以上代碼執行結果為:

這是一個常量

這是一個私有函數!

或者給加載的模組定義一個別名變數,方便調用:

test_module2.lua 檔

-- test_module2.lua 檔
-- module 模組為上文提到到 module.lua
-- 別名變數 m
local m = require("module")
 
print(m.constant)
 
m.func3()

以上代碼執行結果為:

這是一個常量

這是一個私有函數!

加載機制

對於自定義的模組,模組檔不是放在哪個檔目錄都行,函數 require 有它自己的檔路徑加載策略,它會嘗試從 Lua 檔或 C 程式庫中加載模組。

require 用於搜索 Lua 檔的路徑是存放在全局變數 package.path 中,當 Lua 啟動後,會以環境變數 LUA_PATH 的值來初始這個環境變數。如果沒有找到該環境變數,則使用一個編譯時定義的默認路徑來初始化。

當然,如果沒有 LUA_PATH 這個環境變數,也可以自定義設置,在當前用戶根目錄下打開 .profile 檔(沒有則創建,打開 .bashrc 檔也可以),例如把 "~/lua/" 路徑加入 LUA_PATH 環境變數裏:

#LUA_PATH
export LUA_PATH="~/lua/?.lua;;"

檔路徑以 ";" 號分隔,最後的 2 個 ";;" 表示新加的路徑後面加上原來的默認路徑。

接著,更新環境變數參數,使之立即生效。

source ~/.profile

這時假設 package.path 的值是:

/Users/dengjoe/lua/?.lua;./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua

那麼調用 require("module") 時就會嘗試打開以下檔目錄去搜索目標。

/Users/dengjoe/lua/module.lua;
./module.lua
/usr/local/share/lua/5.1/module.lua
/usr/local/share/lua/5.1/module/init.lua
/usr/local/lib/lua/5.1/module.lua
/usr/local/lib/lua/5.1/module/init.lua

如果找過目標檔,則會調用 package.loadfile 來加載模組。否則,就會去找 C 程式庫。

搜索的檔路徑是從全局變數 package.cpath 獲取,而這個變數則是通過環境變數 LUA_CPATH 來初始。

搜索的策略跟上面的一樣,只不過現在換成搜索的是 so 或 dll 類型的檔。如果找得到,那麼 require 就會通過 package.loadlib 來加載它。


C 包

Lua和C是很容易結合的,使用 C 為 Lua 寫包。

與Lua中寫包不同,C包在使用以前必須首先加載並連接,在大多數系統中最容易的實現方式是通過動態連接庫機制。

Lua在一個叫loadlib的函數內提供了所有的動態連接的功能。這個函數有兩個參數:庫的絕對路徑和初始化函數。所以典型的調用的例子如下:

local path = "/usr/local/lua/lib/libluasocket.so"
local f = loadlib(path, "luaopen_socket")

loadlib 函數加載指定的庫並且連接到 Lua,然而它並不打開庫(也就是說沒有調用初始化函數),反之他返回初始化函數作為 Lua 的一個函數,這樣我們就可以直接在Lua中調用他。

如果加載動態庫或者查找初始化函數時出錯,loadlib 將返回 nil 和錯誤資訊。我們可以修改前面一段代碼,使其檢測錯誤然後調用初始化函數:

local path = "/usr/local/lua/lib/libluasocket.so"
-- 或者 path = "C:\\windows\\luasocket.dll",這是 Window 平臺下
local f = assert(loadlib(path, "luaopen_socket"))
f()  -- 真正打開庫

一般情況下我們期望二進位的發佈庫包含一個與前面代碼段相似的 stub 檔,安裝二進位庫的時候可以隨便放在某個目錄,只需要修改 stub 檔對應二進位庫的實際路徑即可。

將 stub 檔所在的目錄加入到 LUA_PATH,這樣設定後就可以使用 require 函數加載 C 庫了。