原文: FFI Tutorial
相關連接:OpenResty 最佳實踐之 FFIhtml
FFI 庫時默認編譯進 LuaJIT 中的,可是不會默認加載或初始化。所以,當須要使用 FFI 庫時,須要在 Lua 文件的開頭添加以下語句:git
local ffi = require("ffi")
以下示例顯示瞭如何訪問標準系統函數。windows
local ffi = require("ffi") ffi.cdef[[ void Sleep(int ms); int poll(struct pollfd *fds, unsigned long nfds, int timeout); ]] local sleep if ffi.os == "Windows" then function sleep(s) ffi.C.Sleep(s*1000) end else function sleep(s) ffi.C.poll(nil, 0, s*1000) end end for i = 1, 160 do io.write("."); io.flush() sleep(0.01) end io.write("\n")
以下示例顯示了若是在 Lua 代碼中訪問 zlib 壓縮庫。api
local ffi = require("ffi") -- 定義由 zlib 提供的 C 函數 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); ]] -- 加載 zlib 共享庫。在 POSIX 系統上,名爲 libz.so,一般是預安裝的。 -- 由於 ffi.load() 會自動添加缺失的標準前綴/後綴,所以能夠簡單地加載 "z" 庫。 local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z") local function compress(txt) -- 首先,經過使用未壓縮字符串的長度來調用 zlib.compressBoud 來獲取 -- 壓縮緩存區的最大大小. local n = zlib.compressBound(#txt) -- 分配這個 n 大小的字節緩存區,類型規範中的 [?] 表示可變長度數組(VLA). -- 該數組的實際元素個數由 ffi.new 的第二個參數給出. local buf = ffi.new("uint8_t[?]", n) -- 看上面 compress2 的函數聲明可知,destLen 被定義爲一個指針。這是由於 -- 傳入的是最大緩存區的大小並返回實際使用的長度. -- 在 C 中能夠經過傳入一個本地變量的地址 (即 &buflen),可是在 Lua 中沒有 -- 地址操做,所以傳入的是隻有一個元素的數組。 local buflen = ffi.new("unsigned long[1]", n) local res = zlib.compress2(buf, buflen, txt, #txt, 9) assert(res == 0) -- 將壓縮數據做爲 Lua 字符串返回,所以使用 ffi.string(),它須要指向 -- 數據開頭和實際長度的指針,這個長度已經經過 buflen 數組返回了 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)
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
以下列表顯示瞭如何將常見的 C 語言轉化爲 LuaJIT FFI:
數組
將庫函數緩存在 local 變量或 upvalues 中是一種常見的用法,以下示例緩存
local byte, char = string.byte, string.char local function foo(x) return char(byte(x) + 1) end
這個能夠經過(更快的)直接使用 local 變量或 upvalue 來替換屢次哈希表查找。這對於 LuaJIT 來講不是那麼重要,由於 JIT 編譯器大量優化哈希表查找,甚至能將大部份內容從內循環中提高出來。可是它並不能消除全部這些。數據結構
經過 FFI 庫調用 C 函數有一點不一樣。JIT 編譯器有特殊的邏輯來消除從 C 庫命名空間中解析的函數的全部查找開銷。所以,緩存單個 C 函數是沒有用的,其實是拔苗助長:函數
local funca, funcb = ffi.funca, ffi.C.funcb -- Not helpful local function foo(x, n) for i = 1, n do funcb(funca(x, i), 1) end end
這會將它們變成間接調用,並生成更大更慢的機器代碼。相反,須要緩存的是命令空間自己並依賴 JIT 編譯器來消除查找:優化
local C = ffi.C -- Instead use this local function foo(x, n) for i = 1, n do C.funcb(C.funca(x, i), 1) end end
這會生成更短更快的代碼。所以不要緩存 C 函數,但要緩存命名空間。大多數狀況下,命名空間已經位於外部做用域的本地變量中。如來自 local lib = ffi.load(...)
。注意,不須要將其複製到函數範圍的本地變量中。ui
必須首先聲明外部符號,而後能夠經過索引 C 庫命名空間來訪問外部符號,該命名空間自動將符號綁定到特定庫。
聲明 C 函數或者 C 的數據結構,數據結構能夠是結構體、枚舉或者是聯合體,函數能夠是 C 標準函數,或者第三方庫函數,也能夠是自定義的函數,注意這裏只是函數的聲明,並非函數的定義。聲明的函數應該要和原來的函數保持一致。
ffi.cdef[[ typedef struct foo { int a, b; } foo_t; /* Declare a struct and typedef. */ int dofoo(foo_t *f, int n); /* Declare an external C function */ ]]
注意,外部符號僅被聲明,但它們並不受任何特定地址的約束。使用 C 庫命名空間實現綁定.此外全部使用的庫函數都要對其進行聲明。
以下示例,建立一個 myffi.c,內容:
int add(int x, int y) { return x + y; }
接着在 Linux 下生成動態連接庫:
gcc -g -o libmyffi.so -fpic -shared myffi.c
在 LD_LIBRARY_PATH 環境變量中添加生成庫的路徑:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:your_lib_path
在 Lua 代碼中增長以下行:
ffi.load(name, [,global])
ffi.load 會經過給定的 name 加載動態庫,返回一個綁定到這個庫符號的新的 C 庫命名空間,在 POSIX 系統中,若是 global 被設置爲 true,這個庫符號被加載到一個全局命名空間。另外這個 name 能夠是一個動態庫的路徑,那麼會根據路徑來查找,不然的話會在默認的搜索路徑中去找動態庫。在 POSIX 系統中,若是在 name 這個字段中沒有寫上點符號 .,那麼 .so 將會被自動添加進去,例如 ffi.load("z") 會在默認的共享庫搜尋路徑中去查找 libz.so,在 windows 系統,若是沒有包含點號,那麼 .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 (調用 ffi.cdef 中聲明的系統函數)來直接調用 add 函數(注:要在 ffi.load 中加上參數 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.
這是默認的 C 庫命名空間--注意爲大寫的 C。它綁定到目標系統上的默認符號集或庫。這些或多或少與 C 編譯器默認提供的相同,而不指定額外的連接庫。
在 POSIX 系統中,它綁定到默認或全局命名空間中的符號。這包括可執行文件中的全部導出符號以及加載到全局命名空間中的任意庫。這至少包括 libc,libm,libdl(在 Linux 中),libgcc(若是使用 GCC 編譯器),以及 LuaJIT 自己提供的 Lua/C API 中的任何導出符號。
這將加載由 name 指定的動態庫,並返回一個綁定到其符號的新 C 庫命名空間。在 POSIX 系統中,若是 global 爲 true,這個庫的符號將會加載到全局命名空間中。
若是 name 是路徑,該庫將會從該路徑中加載。不然,name 將以與系統相關的方式進行規範化,並按默認搜索路徑來搜索動態庫:在 POSIX 系統上,若是 name 不包含 '.',則追加擴展名 .so。此外,若是須要,還會添加庫的前綴。因此 ffi.load("z") 在默認的共享庫路徑中搜索 "libz.so"。
ctype = ffi.typeof(ct)
建立一個 ctype 對象,會解析一個抽象的 C 類型定義。該函數僅用於解析 cdecl 一次,而後使用生成的 ctype 對象做爲構造函數。
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[?]")
以下 API 函數建立 cdata 對象(ctype() 返回 "cdata")。全部建立的對象都是垃圾回收的。
cdata = ffi.new(ct [,nelem] [,init...]) cdata = ctype([nelem,] [init...])
ffi.new 開闢空間,第一個參數爲 ctype 對象,ctype 對象最好經過 ctype = ffi.typeof(ct) 構建。
若是使用 ffi.new 分配的 cdata 對象指向的內存塊是由垃圾回收器 LuaJIT GC 自動管理的,全部不須要用戶去釋放內存。
若是使用 ffi.C.malloc 分配的空間便再也不使用 LuaJIT 本身的分配器了,因此不是由 LuaJIT GC 來管理的,可是,要注意的是 ffi.C.malloc 返回的指針自己所對應的 cdata 對象仍是由 LuaJIT GC 來管理的,也就是這個指針的 cdata 對象指向的是用 ffi.C.malloc 分配的內存空間。這個時候,你應該經過 ffi.gc() 函數在這個指針的 cdata 對象上面註冊本身的析構函數,這個析構函數裏面能夠再調用 ffi.C.free,這樣的話當 C 指針所對應的 cdata 對象被 LuaJIT GC 管理器垃圾回收的時候,也會自動調用你註冊的那個析構函數來執行 C 級別的內存釋放。
請儘量使用最新版本的 LuaJIT,x86_64 上由 LuaJIT GC 管理的內存已經由 1G->2G,雖然管理的內存變大了,可是若是要使用很大的內存,仍是用 ffi.C.malloc 來分配會比較好,避免耗盡了 LuaJIT GC 管理內存的上限。
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)
cdata = ffi.cast(ct, init)
建立一個 scalar cdata 對象。
local c_str_t = ffi.typeof("const char*") local c_str = ffi.case(c_str_t, str) -- 轉換爲指針地址 local uintptr_t ffi.typeof("uintptr_t") tonumber(ffi.cast(uintptr_t, c_str) -- 轉換爲數字
ctype = ffi.metatype(ct, metatable)
爲給定的 ct 建立一個 ctype 對象,並將其與 metatable 相關聯。僅容許使用 struct/union 類型,複數和向量。若是須要,其餘類型能夠封裝在 struct 中。
與 metatable 的關聯是永久性的,以後不可更改。以後,metatable 的內容和 __index 表(若是有的話)的內容都不能被修改。不管對象如何建立或源自何處,相關地元表都會自動應用於此類型的全部用途。注意,對類型的預約義操做具備優先權(如,聲明的字段名稱不能被覆蓋)。