1. 程式人生 > >熟練使用Lua(三)模組支援:require的載入module的基本原理(1)

熟練使用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

(刪除了最後的".XXXX"部分)}. 如果name是一個以點分割的串,那麼得到的mod類似這個樣子:

  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