熟練使用Lua(三)模組支援:require的載入module的基本原理(1)
Lua標準庫- 模組(Modules)
轉: https://www.cnblogs.com/jadeboy/p/4150048.html
Lua包庫為lua提供簡易的載入及建立模組的方法,由require、module方法及package表組成
1、module (name [, ···])
功能:建立一個模組。
module的處理流程:
module(name, cb1, cb2, …)
a. 如果package.loaded[name]是一個table,那麼就把這個table作為一個module
b. 如果全域性變數name是一個table,就把這個全域性變數作為一個module
c. 當以前兩種情況都不存表name時,將新建一個表,並使其作為全域性名name的值,並package.loaded[name],而且設t._NAME為name,t._M為module,t._PACKAGE為包的全名(模組名-元件a.b.c);最後把此module設t作為當前函式的新環境表和package.loaded[name]的新值(也就是說,舊的環境表將不能訪問,除了加上package.seeall引數外),以被require使用
即:建立table:t = {[name]=package.loaded[name], ["_NAME"]=name, ["_M"]=t, ["_PACKAGE"]=name
hello.world==> {["hello"]={["world"]={XXXXXXX}}}
d. 依次呼叫cbs:
cb1(mod), cb2(mod),…
e. 將當前模組的環境設定為module,同時把package.loaded[name] = module
module(name)後的可選引數為接收module名的函式,如package.seeall
當在模組檔案中使用module函式的時候,如下所示;
module “mymodule”
實際上等同於以下的語句:
local modname = “mymodule” -– 定義模組名
local M = {} -- 定義用於返回的模組表
_G[modname] = M -- 將模組表加入到全域性變數中
package.loaded[modname] = M -- 將模組表加入到package.loaded中,
防止多次載入
setfenv(1,M) -- 將模組表設定為函式的環境表,
這使得模組中的所有操作是以在模組表中的,這樣定義函式就直接定義在模組表中
通過module(),可以方便的編寫模組中的內容。module 指令執行完後,整個環境被壓棧,所以前面全域性的東西再看不見了。比如定義了一個 test 模組,使用module(“test”)後,下面不再看的見前面的全域性環境。 如果在這個模組裡想呼叫 print 輸出除錯資訊怎麼辦呢?一個簡單的方法是
local print=print
module("test")
這樣 print 是一個 local 變數,下面也是可見的。或者可以用
local _G=_G
module("test")
那麼 _G.print 也是可以用的。
當然還有一種巧妙的方式,lua 5.1 提供了一個 package.seeall 可以作為 module 的option 傳入module(“test”,package.seeall)
這樣就 OK 了。至於它們是如何工作的,還是自己讀原始碼會理解的清楚一些。
具體的使用比如:宣告myModule包
複製程式碼
– File : myModule.lua
module( "myModule", package.seeall) --顯示宣告一個myModule包
function printMsg(msg)
print("myModule :" .. msg)
end
function setMag(msg)
test.msg = msg
end
複製程式碼
呼叫該myModule包:
複製程式碼
– File : myModuleTest.lua
local print = print
local mod = require("myModule")
print("call myModule start")
test = {}
mod.setMag("test module~")
print(test.msg)
mod.printMsg("yes")
複製程式碼
輸出結果如下:
call myModule start
test module~
myModule :yes
通過module("…", package.seeall)來顯示宣告一個包。看很多github上面早期的開源專案使用的都是這種方式,但官方不推薦再使用這種方式。
因為:1, package.seeall這種方式破壞了模組的高內聚,原本引入oldmodule只想呼叫它的foo()函式,但是它卻可以讀寫全域性屬性,例如oldmodule.os.
2, module函式的side-effect引起的,它會汙染全域性環境變數。
module(“hello.world”)會建立一個hello的table,並將這個table注入全域性環境變數中,這樣使得不想引用它的模組也能呼叫hello模組的方法。
所以還是 通過return table來實現一個模組 的更優。使用如下:
複製程式碼
–File : module.lua
local module = {}
function module.foo()
print("module.foo called")
end
function module:setMsg(msg)
self.mMsg = msg
end
function module:getMsg()
return self.mMsg
end
return module
複製程式碼
– File : myModuleTest.lua
local mod = require("module")
mod:setMsg("test module~")
-- mod.setMsg( mod , "test module~") -- 等價上句
print(mod:getMsg())
– 得到結果如下:test module~
2、require (modname)
功能:載入指定的模組。
此函式先檢測package.loaded表中是否存在modname,存在則直接返回當中的值,沒有則通過定義的載入器載入modname。
1) require機制相關的資料和函式
package.path:儲存載入外部模組(lua中"模組"和"檔案"這兩個概念的分界比較含糊,因為這個值在不同的時刻會扮演不同的角色)的搜尋 路徑,這種路徑是"模板式的路徑",它裡面會包含可替代符號"?",這個符號會被替換,然後lua查詢這個檔案是否存在,如果存在就會呼叫其中特定的接 口。典型的值為:
“./?.lua;./?.lc;/usr/local/?/init.lua”
如果lua程式碼中呼叫:require(“hello.world”)
那麼lua會依次查詢:
./hello/world.lua ==>這裡"hello.world"變成了"hello/world",並替換了模型"./?.lua"
./hello/world.lc
…
(這種處理方式和python類似,只不過不需要__init__.py,也有呼叫python中的__init__.py)
package.path在虛擬機器啟動的時候設定,如果存在環境變數LUA_PATH,那麼就用該環境變數作為
它的值,並把這個環境變數中的";;"替換為luaconf.h中定義的預設值,如果不存在該變數就直接使用
luaconf.h定義的預設值
package.cpath:作用和packag.path一樣,但它是用於載入第三方c庫的。它的初始值可以通過環境變數
LUA_CPATH來設定
package.loadlib(libname, func):相當與手工開啟c庫libname, 並匯出函式func返回,loadlib其實是ll_loadlib
2) 查詢載入器順序:
require(在lua中它是ll_require函式)的查詢順序如下:
a.首先在package.loaded查詢modelname,如果該模組已經存在,就直接返回它的值
b.在package.preload查詢modelname, 如果preload存在,那麼就把它作為loader,呼叫loader(L)
c.根據package.path的模式查詢lua庫modelname,這個庫是通過module函式定義的,對於頂層的lua庫,檔名和庫名是一 樣的而且不需要呼叫顯式地在lua檔案中呼叫module函式(在ll_require函式中可以看到處理方式),也就是說lua會根據lua檔案直接完 成一個loader的初始化過程。
d.根據package.cpath查詢c庫,這個庫是符合lua的一些規範的(export具有一定特徵的函式介面),lua先已動態的方式載入該c庫,然後在庫中查詢並呼叫相應名字的介面,例如:luaopen_hello_world
e.已第一個"."為分割,將模組名劃分為:(main, sub)的形式,根據package.cpath查詢main,如果存在,就載入該庫並查詢相應的介面:luaopen_main_sub,例如:先查詢 hello庫,並查詢luaopen_hello_world介面
f.得到loder後,用modname作為唯一的引數呼叫該loader函式。當然引數是通過lua的棧傳遞的,所以loader的原型必須符合lua的規範:int LUA_FUNC(lua_State *L)
ll_require會將這個loader的返回值符給package.loaded[modelname],如果loader不返回值同時 package.loaded[modelname]不存在時, ll_require就會把package.loaded[modelname]設為true。最後ll_reuqire把package.loaded [modelname]返回給呼叫者。當載入失敗時,require將觸發錯誤
3) require的另一個功能是避免重複載入同一個檔案兩次。
Lua保留一張所有已經載入的檔案的列表(使用table儲存)。如果一個載入的檔案在表中存在require簡單的返回;表中保留載入的檔案的虛名,而不是實檔名。所以如果你使用不同的虛檔名require同一個檔案兩次,將會載入兩次該檔案。比如require “foo"和require “foo.lua”,路徑為”?;?.lua"將會載入foo.lua兩次。我們也可以通過全域性變數_LOADED訪問檔名列表,這樣我們就可以判斷檔案是否被載入過;同樣我們也可以使用一點小技巧讓require載入一個檔案兩次。比如,require "foo"之後_LOADED[“foo”]將不為nil,我們可以將其賦值為nil,require"foo.lua"將會再次載入該檔案。
也可以根據自己的需要將package.loaded[modname] or _LOADED[modname]置nil,然後重新require,來完成lua熱更新功能。具體的可參見:codedump
3、package.cpath
功能:用於require C loader的搜尋路徑
可以通過修改LUA_CPATH變數(luaconf.h)修改此值
4、package.loaded
功能:一個用於讓require知道哪些模組已載入的記錄表,如果package.loaded已經有require要的值,則直接返回此值
5、package.loadlib (libname, funcname)
功能:通過動態連線C函式庫方式載入Lua擴充套件庫
libname為庫檔名,funcname為入口函式(此函式必須為純C介面函式 c++則需用 extern “C” {} 進行限制)
6、package.path
功能:用於require Lua loader的搜尋路徑
可以通過修改LUA_PATH變數(luaconf.h)修改此值
7、package.preload
功能:一個用於儲存特殊模組載入器的表
8、package.seeall(module)
功能:為module設定一個元表,此元表的__index欄位的值為全域性環境_G。所以module可以訪問全域性環境
注:以此函式作為module()的一個選項(詳細見module())
參考文章: ClickHere and Here