Lua中:require、dofile、module加載流程基礎

轉自:https://www.cnblogs.com/yyxt/p/3870236.html

Lua提供高級的require函數來加載運行庫。粗略的說require和dofile完成同樣的功能但有兩點不同:

1. require會搜索目錄加載文件;

2. require會判斷是否文件已經加載避免重複加載同一文件。

由於上述特徵,require在Lua中是加載庫的更好的函數。

(一) require

  require使用的路徑和普通我們看到的路徑還有些區別,我們一般見到的路徑都是一個目錄列表。require的路徑是一個模式列表,每一個模式指明一種由虛文件名(require的參數)轉成實文件名的方法。更明確地說,每一個模式是一個包含可選的問號的文件名。匹配的時候Lua會首先將問號用虛文件名替換,然後看是否有這樣的文件存在。如果不存在繼續用同樣的方法用第二個模式匹配。例如,路徑如下:

?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua

調用require "test"時會試着打開這些文件:

test
test.lua
c:\windows\test
/usr/local/lua/test/test.lua

require關注的問題只有分號(模式之間的分隔符)和問號,其他的信息(目錄分隔符,文件擴展名)在路徑中定義。

  爲了確定路徑,Lua首先檢查全局變量LUA_PATH是否爲一個字符串,如果是則認爲這個串就是路徑;否則require檢查環境變量LUA_PATH的值,如果兩個都失敗;require使用固定的路徑(典型的"?;?.lua")

  require的另一個功能是避免重複加載同一個文件兩次。Lua保留一張所有已經加載的文件的列表(使用table保存)。如果一個加載的文件在表中存在, 則require簡單的返回;表中保留加載的文件的虛名,而不是實文件名。所以如果你使用不同的虛文件名require同一個文件兩次,將會加載兩次該文件。比如require "foo"和require "foo.lua",路徑爲"?;?.lua"將會加載foo.lua兩次。我們也可以通過全局變量_LOADED訪問文件名列表,這樣我們就可以判斷文件是否被加載過;同樣我們也可以使用一點小技巧讓require加載一個文件兩次。比如,require "foo"之後_LOADED["foo"]將不爲nil,我們可以將其賦值爲nil,require "foo.lua"將會再次加載該文件。

一個路徑中的模式也可以不包含問號而只是一個固定的路徑,比如:

?;?.lua;/usr/local/default.lua

  這種情況下,require沒有匹配的時候就會使用這個固定的文件(當然這個固定的路徑必須放在模式列表的最後纔有意義)。在require運行一個chunk以前,它定義了一個全局變量_REQUIREDNAME用來保存被required的虛文件的文件名。我們可以通過使用這個技巧擴展require的功能。舉個極端的例子,我們可以把路徑設爲"/usr/local/lua/newrequire.lua",這樣以後每次調用require都會運行newrequire.lua,這種情況下可以通過使用_REQUIREDNAME的值去實際加載required的文件。

(二) dofile

  我們知道一個lua文件是作爲一個代碼塊(chunk)存在的,其實質就是一個函數,那麼最簡單的,我在一個外部lua文件中寫一段代碼,然後在主lua文件中用dofile調用,外部文件的代碼塊就會執行了。

外部lua文件在編譯時並沒有涉及詞法域:

  形式上很類似C語言的#include<...>,在其他地方定義的函數,經這麼引入文件之後就可以調用了。不過lua並不是定義和實現分離的語言,這樣是把整個定義部分都加載進來了。加載過程大致上是: lua先加載這個外部文件,然後運行它。實際上這段外部代碼是可以有返回值的,它的返回值就是require的返回值。這裏我們什麼返回值都沒有,執行這個外部代碼的結果就是定義了這麼個全局函數。注意是全局函數,雖然通常我們直接定義的函數 都是全局函數所以都沒怎麼注意過,要是非要定義個局部函數在主程序塊裏可就看不到了。另外和前面的情形一樣,外部代碼塊只認識“雷叔”,“牛叔”是誰它根本不知道。
        可以在外部文件裏定義一堆函數,然後全都加到全局環境下。不過全局的東西用起來要小心,有一個原則是對全局的“污染”越小越好。那麼自然就引入了“模塊” 的概念。在lua中,模塊由萬能的table來充當。最自然的想法就是定義一個table,然後把要定義的函數放在這個table裏,最後返回這個 table就行了。

(三) lua中的require機制
    爲了方便代碼管理,通常會把lua代碼分成不同的模塊,然後在通過require函數把它們加載進來。現在看看lua的require的處理流程。


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的處理流程:

require(modelname)

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]返回給調用者。

3.module的處理流程

module(name, cb1, cb2, ...)

  a. 如果package.loaded[name]是一個table,那麼就把這個table作爲一個mod
  b. 如果全局變量name是一個table,就把這個全局變量作爲一個mod
  c. 創建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. 將當前模塊的環境設置爲mod,同時把package.loaded[name] = mod

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章