FFI 庫,是 LuaJIT 中最重要的一個擴展庫。它允許從純 Lua 代碼調(diào)用外部 C 函數(shù),使用 C 數(shù)據(jù)結(jié)構(gòu)。有了它,就不用再像 Lua 標準 ?math
?庫一樣,編寫 Lua 擴展庫。把開發(fā)者從開發(fā) Lua 擴展 C 庫(語言/功能綁定庫)的繁重工作中釋放出來。學習完本小節(jié)對開發(fā)純 ?ffi
?的庫是有幫助的,像 lru-resty-lrucache 中的 ?pureffi.lua
?,這個純 ?ffi
? 庫非常高效地完成了 ?lru
?緩存策略。
簡單解釋一下 Lua 擴展 C 庫,對于那些能夠被 Lua 調(diào)用的 C 函數(shù)來說,它的接口必須遵循 Lua 要求的形式,就是 ?typedef int (*lua_CFunction)(lua_State* L)
?,這個函數(shù)包含的參數(shù)是 ?lua_State
?類型的指針 L ??梢酝ㄟ^這個指針進一步獲取通過 Lua 代碼傳入的參數(shù)。這個函數(shù)的返回值類型是一個整型,表示返回值的數(shù)量。需要注意的是,用 C 編寫的函數(shù)無法把返回值返回給 Lua 代碼,而是通過虛擬棧來傳遞 Lua 和 C 之間的調(diào)用參數(shù)和返回值。不僅在編程上開發(fā)效率變低,而且性能上比不上 FFI 庫調(diào)用 C 函數(shù)。
FFI 庫最大限度的省去了使用 C 手工編寫繁重的 ?Lua/C
? 綁定的需要。不需要學習一門獨立/額外的綁定語言——它解析普通 C 聲明。這樣可以從 C 頭文件或參考手冊中,直接剪切,粘貼。它的任務(wù)就是綁定很大的庫,但不需要搗鼓脆弱的綁定生成器。
FFI 緊緊的整合進了 LuaJIT(幾乎不可能作為一個獨立的模塊)。?JIT
?編譯器在 C 數(shù)據(jù)結(jié)構(gòu)上所產(chǎn)生的代碼,等同于一個 C 編譯器應(yīng)該生產(chǎn)的代碼。在 ?JIT
?編譯過的代碼中,調(diào)用 C 函數(shù),可以被內(nèi)連處理,不同于基于 ?Lua/C API
? 函數(shù)調(diào)用。
noun | Explanation |
---|---|
cdecl | A definition of an abstract C type(actually, is a lua string) |
ctype | C type object |
cdata | C data object |
ct | C type format, is a template object, may be cdecl, cdata, ctype |
cb | callback object |
VLA | An array of variable length |
VLS | A structure of variable length |
功能: Lua ffi 庫的 API,與 LuaJIT 不可分割。
毫無疑問,在 ?lua
?文件中使用 ?ffi
?庫的時候,必須要有下面的一行。
local ffi = require "ffi"
語法: ffi.cdef(def)
功能: 聲明 C 函數(shù)或者 C 的數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)可以是結(jié)構(gòu)體、枚舉或者是聯(lián)合體,函數(shù)可以是 C 標準函數(shù),或者第三方庫函數(shù),也可以是自定義的函數(shù),注意這里只是函數(shù)的聲明,并不是函數(shù)的定義。聲明的函數(shù)應(yīng)該要和原來的函數(shù)保持一致。
ffi.cdef[[
typedef struct foo { int a, b; } foo_t; /* Declare a struct and typedef. */
int printf(const char *fmt, ...); /* Declare a typical printf function. */
]]
注意: 所有使用的庫函數(shù)都要對其進行聲明,這和我們寫 C 語言時候引入 .h 頭文件是一樣的。
順帶一提的是,并不是所有的 C 標準函數(shù)都能滿足我們的需求,那么如何使用 第三方庫函數(shù) 或 自定義的函數(shù) 呢,這會稍微麻煩一點,不用擔心,你可以很快學會。: ) 首先創(chuàng)建一個 ?myffi.c
?,其內(nèi)容是:
int add(int x, int y)
{
return x + y;
}
接下來在 Linux 下生成動態(tài)鏈接庫:
gcc -g -o libmyffi.so -fpic -shared myffi.c
為了方便我們測試,我們在 ?LD_LIBRARY_PATH
?這個環(huán)境變量中加入了剛剛庫所在的路徑,因為編譯器在查找動態(tài)庫所在的路徑的時候其中一個環(huán)節(jié)就是在 ?LD_LIBRARY_PATH
?這個環(huán)境變量中的所有路徑進行查找。命令如下所示。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:your_lib_path
在 Lua 代碼中要增加如下的行:
ffi.load(name [,global])
?ffi.load
? 會通過給定的 ?name
?加載動態(tài)庫,返回一個綁定到這個庫符號的新的 C 庫命名空間,在 POSIX 系統(tǒng)中,如果 ?global
?被設(shè)置為 ?ture
?,這個庫符號被加載到一個全局命名空間。另外這個 ?name
?可以是一個動態(tài)庫的路徑,那么會根據(jù)路徑來查找,否則的話會在默認的搜索路徑中去找動態(tài)庫。在 POSIX 系統(tǒng)中,如果在 ?name 這個字段中沒有寫上點符號 .,那么 ?.so
? 將會被自動添加進去,例如 ?ffi.load("z")
?會在默認的共享庫搜尋路徑中去查找 ?libz.so
?,在 ?windows
?系統(tǒng),如果沒有包含點號,那么 ?.dll
? 會被自動加上。
下面看一個完整例子:
local ffi = require "ffi"
local myffi = ffi.load('myffi')
ffi.cdef[[
int add(int x, int y); /* don't forget to declare */
]]
local res = myffi.add(1, 2)
print(res) -- output: 3 Note: please use luajit to run this script.
除此之外,還能使用 ?ffi.C
? (調(diào)用 ffi.cdef 中聲明的系統(tǒng)函數(shù)) 來直接調(diào)用 ?add
?函數(shù),記得要在 ?ffi.load
? 的時候加上參數(shù) ?true
?,例如 ?ffi.load('myffi', true)
?。
完整的代碼如下所示:
local ffi = require "ffi"
ffi.load('myffi',true)
ffi.cdef[[
int add(int x, int y); /* don't forget to declare */
]]
local res = ffi.C.add(1, 2)
print(res) -- output: 3 Note: please use luajit to run this script.
語法: ctype = ffi.typeof(ct)
功能: 創(chuàng)建一個 ctype 對象,會解析一個抽象的 C 類型定義。
local uintptr_t = ffi.typeof("uintptr_t")
local c_str_t = ffi.typeof("const char*")
local int_t = ffi.typeof("int")
local int_array_t = ffi.typeof("int[?]")
語法: cdata = ffi.new(ct [,nelem] [,init...])
功能: 開辟空間,第一個參數(shù)為 ctype 對象,ctype 對象最好通過 ctype = ffi.typeof(ct) 構(gòu)建。
順便一提,可能很多人會有疑問,到底 ?ffi.new
? 和 ?ffi.C.malloc
? 有什么區(qū)別呢?
如果使用 ?ffi.new
? 分配的 ?cdata
?對象指向的內(nèi)存塊是由垃圾回收器 ?LuaJIT GC
? 自動管理的,所以不需要用戶去釋放內(nèi)存。
如果使用 ?ffi.C.malloc
? 分配的空間便不再使用 LuaJIT 自己的分配器了,所以不是由 ?LuaJIT GC
? 來管理的,但是,要注意的是 ?ffi.C.malloc
? 返回的指針本身所對應(yīng)的 ?cdata
?對象還是由 ?LuaJIT GC
? 來管理的,也就是這個指針的 ?cdata
?對象指向的是用 ?ffi.C.malloc
? 分配的內(nèi)存空間。這個時候,你應(yīng)該通過 ?ffi.gc()
? 函數(shù)在這個 C 指針的 ?cdata
?對象上面注冊自己的析構(gòu)函數(shù),這個析構(gòu)函數(shù)里面你可以再調(diào)用 ?ffi.C.free
?,這樣的話當 C 指針所對應(yīng)的 ?cdata
?對象被 ?Luajit GC
? 管理器垃圾回收時候,也會自動調(diào)用你注冊的那個析構(gòu)函數(shù)來執(zhí)行 C 級別的內(nèi)存釋放。
請盡可能使用最新版本的 ?Luajit
?,?x86_64
?上由 ?LuaJIT GC
? 管理的內(nèi)存已經(jīng)由 ?1G->2G
?,雖然管理的內(nèi)存變大了,但是如果要使用很大的內(nèi)存,還是用 ?ffi.C.malloc
? 來分配會比較好,避免耗盡了 ?LuaJIT GC
? 管理內(nèi)存的上限,不過還是建議不要一下子分配很大的內(nèi)存。
local int_array_t = ffi.typeof("int[?]")
local bucket_v = ffi.new(int_array_t, bucket_sz)
local queue_arr_type = ffi.typeof("lrucache_pureffi_queue_t[?]")
local q = ffi.new(queue_arr_type, size + 1)
語法: ffi.fill(dst, len [,c])
功能: 填充數(shù)據(jù),此函數(shù)和 memset(dst, c, len) 類似,注意參數(shù)的順序。
ffi.fill(self.bucket_v, ffi_sizeof(int_t, bucket_sz), 0)
ffi.fill(q, ffi_sizeof(queue_type, size + 1), 0)
語法: cdata = ffi.cast(ct, init)
功能: 創(chuàng)建一個 scalar cdata 對象。
local c_str_t = ffi.typeof("const char*")
local c_str = ffi.cast(c_str_t, str) -- 轉(zhuǎn)換為指針地址
local uintptr_t = ffi.typeof("uintptr_t")
tonumber(ffi.cast(uintptr_t, c_str)) -- 轉(zhuǎn)換為數(shù)字
所有由顯式的 ?ffi.new()
?, ?ffi.cast()
?etc. 或者隱式的 ?accessors
?所創(chuàng)建的 ?cdata
?對象都是能被垃圾回收的,當他們被使用的時候,你需要確保有在 ?Lua stack,upvalue
?,或者 ?Lua table
? 上保留有對 ?cdata
?對象的有效引用,一旦最后一個 ?cdata
?對象的有效引用失效了,那么垃圾回收器將自動釋放內(nèi)存(在下一個 ?GC
?周期結(jié)束時候)。另外如果你要分配一個 ?cdata
?數(shù)組給一個指針的話,你必須保持這個持有這個數(shù)據(jù)的 ?cdata
?對象活躍,下面給出一個官方的示例:
ffi.cdef[[
typedef struct { int *a; } foo_t;
]]
local s = ffi.new("foo_t", ffi.new("int[10]")) -- WRONG!
local a = ffi.new("int[10]") -- OK
local s = ffi.new("foo_t", a)
-- Now do something with 's', but keep 'a' alive until you're done.
相信看完上面的 ?API
?你已經(jīng)很累了,再堅持一下吧!休息幾分鐘后,讓我們來看看下面對官方文檔中的示例做剖析,希望能再加深你對 ?ffi
?的理解。
真的很用容易去調(diào)用一個外部 C 庫函數(shù),示例代碼:
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")
以上操作步驟,如下:
事實上,背后的實現(xiàn)遠非如此簡單:③ 使用標準 C 庫的命名空間 ?ffi.C
?。通過符號名 ?printf
?索引這個命名空間,自動綁定標準 C 庫。索引結(jié)果是一個特殊類型的對象,當被調(diào)用時,執(zhí)行 ?printf
?函數(shù)。傳遞給這個函數(shù)的參數(shù),從 Lua 對象自動轉(zhuǎn)換為相應(yīng)的 C 類型。
再來一個源自官方的示例代碼:
local ffi = require("ffi")
ffi.cdef[[
unsigned long compressBound(unsigned long sourceLen);
int compress2(uint8_t *dest, unsigned long *destLen,
const uint8_t *source, unsigned long sourceLen, int level);
int uncompress(uint8_t *dest, unsigned long *destLen,
const uint8_t *source, unsigned long sourceLen);
]]
local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z")
local function compress(txt)
local n = zlib.compressBound(#txt)
local buf = ffi.new("uint8_t[?]", n)
local buflen = ffi.new("unsigned long[1]", n)
local res = zlib.compress2(buf, buflen, txt, #txt, 9)
assert(res == 0)
return ffi.string(buf, buflen[0])
end
local function uncompress(comp, n)
local buf = ffi.new("uint8_t[?]", n)
local buflen = ffi.new("unsigned long[1]", n)
local res = zlib.uncompress(buf, buflen, comp, #comp)
assert(res == 0)
return ffi.string(buf, buflen[0])
end
-- Simple test code.
local txt = string.rep("abcd", 1000)
print("Uncompressed size: ", #txt)
local c = compress(txt)
print("Compressed size: ", #c)
local txt2 = uncompress(c, #txt)
assert(txt2 == txt)
解釋一下這段代碼。我們首先使用 ?ffi.cdef
? 聲明了一些被 zlib 庫提供的 C 函數(shù)。然后加載 zlib 共享庫,在 Windows 系統(tǒng)上,則需要我們手動從網(wǎng)上下載 zlib1.dll 文件,而在 POSIX 系統(tǒng)上 libz 庫一般都會被預(yù)安裝。因為 ffi.load 函數(shù)會自動填補前綴和后綴,所以我們簡單地使用 z 這個字母就可以加載了。我們檢查 ?ffi.os
?,以確保我們傳遞給 ?ffi.load
? 函數(shù)正確的名字。
一開始,壓縮緩沖區(qū)的最大值被傳遞給 ?compressBound
?函數(shù),下一行代碼分配了一個要壓縮字符串長度的字節(jié)緩沖區(qū)。?[?]
? 意味著他是一個變長數(shù)組。它的實際長度由 ?ffi.new
? 函數(shù)的第二個參數(shù)指定。
我們仔細審視一下 ?compress2
?函數(shù)的聲明就會發(fā)現(xiàn),目標長度是用指針傳遞的!這是因為我們要傳遞進去緩沖區(qū)的最大值,并且得到緩沖區(qū)實際被使用的大小。
在 C 語言中,我們可以傳遞變量地址。但因為在 Lua 中并沒有地址相關(guān)的操作符,所以我們使用只有一個元素的數(shù)組來代替。我們先用最大緩沖區(qū)大小初始化這唯一一個元素,接下來就是很直觀地調(diào)用 ?zlib.compress2
? 函數(shù)了。使用 ?ffi.string
? 函數(shù)得到一個存儲著壓縮數(shù)據(jù)的 Lua 字符串,這個函數(shù)需要一個指向數(shù)據(jù)起始區(qū)的指針和實際長度。實際長度將會在 ?buflen
?這個數(shù)組中返回。因為壓縮數(shù)據(jù)并不包括原始字符串的長度,所以我們要顯式地傳遞進去。
?cdata
?類型用來將任意 C 數(shù)據(jù)保存在 Lua 變量中。這個類型相當于一塊原生的內(nèi)存,除了賦值和相同性判斷,Lua 沒有為之預(yù)定義任何操作。然而,通過使用 ?metatable
?(元表),程序員可以為 ?cdata
?自定義一組操作。?cdata
?不能在 Lua 中創(chuàng)建出來,也不能在 Lua 中修改。這樣的操作只能通過 C API。這一點保證了宿主程序完全掌管其中的數(shù)據(jù)。
我們將 C 語言類型與 ?metamethod
?(元方法)關(guān)聯(lián)起來,這個操作只用做一次。?ffi.metatype
? 會返回一個該類型的構(gòu)造函數(shù)。原始 C 類型也可以被用來創(chuàng)建數(shù)組,元方法會被自動地應(yīng)用到每個元素。
尤其需要指出的是,?metatable
?與 C 類型的關(guān)聯(lián)是永久的,而且不允許被修改,?__index
?元方法也是。
下面是一個使用 C 數(shù)據(jù)結(jié)構(gòu)的實例
local ffi = require("ffi")
ffi.cdef[[
typedef struct { double x, y; } point_t;
]]
local point
local mt = {
__add = function(a, b) return point(a.x+b.x, a.y+b.y) end,
__len = function(a) return math.sqrt(a.x*a.x + a.y*a.y) end,
__index = {
area = function(a) return a.x*a.x + a.y*a.y end,
},
}
point = ffi.metatype("point_t", mt)
local a = point(3, 4)
print(a.x, a.y) --> 3 4
print(#a) --> 5
print(a:area()) --> 25
local b = a + point(0.5, 8)
print(#b) --> 12.5
附表:Lua 與 C 語言語法對應(yīng)關(guān)系
Idiom | C code | Lua code |
---|---|---|
Pointer dereference | x = *p | x = p[0] |
int *p | *p = y | p[0] = y |
Pointer indexing | x = p[i] | x = p[i] |
int i, *p | p[i+1] = y | p[i+1] = y |
Array indexing | x = a[i] | x = a[i] |
int i, a[] | a[i+1] = y | a[i+1] = y |
struct/union dereference | x = s.field | x = s.field |
struct foo s | s.field = y | s.field = y |
struct/union pointer deref | x = sp->field | x = sp.field |
struct foo *sp | sp->field = y | s.field = y |
int i, *p | y = p - i | y = p - i |
Pointer dereference | x = p1 - p2 | x = p1 - p2 |
Array element pointer | x = &a[i] | x = a + i |
所謂“能力越大,責任越大”,F(xiàn)FI 庫在允許我們調(diào)用 C 函數(shù)的同時,也把內(nèi)存管理的重擔壓到我們的肩上。 還好 FFI 庫提供了很好用的 ffi.gc 方法。該方法允許給 cdata 對象注冊在 GC 時調(diào)用的回調(diào),它能讓你在 Lua 領(lǐng)域里完成 C 手工釋放資源的事。
C++ 提倡用一種叫 RAII 的方式管理你的資源。簡單地說,就是創(chuàng)建對象時獲取,銷毀對象時釋放。我們可以在 LuaJIT 的 FFI 里借鑒同樣的做法,在調(diào)用 ?resource = ffi.C.xx_create
? 等申請資源的函數(shù)之后,立即補上一行 ?ffi.gc(resource, ...)
? 來注冊釋放資源的函數(shù)。盡量避免嘗試手動釋放資源!即使不考慮 error 對執(zhí)行路徑的影響,在每個出口都補上一模一樣的邏輯會夠你受的(用 goto 也差不多,只是稍稍好一點)。
有些時候,?ffi.C.xx_create
? 返回的不是具體的 cdata,而是整型的 handle。這會兒需要用 ?ffi.metatype
? 把 ?ffi.gc
? 包裝一下:
local resource_type = ffi.metatype("struct {int handle;}", {
__gc = free_resource
})
local function free_resource(handle)
...
end
resource = ffi.new(resource_type)
resource.handle = ffi.C.xx_create()
如果你沒能把申請資源和釋放資源的步驟放一起,那么內(nèi)存泄露多半會在前方等你。寫代碼的時候切記這一點。
更多建議: