OpenResty Lua FFI

2021-08-12 16:51 更新

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)用。

ffi 庫 詞匯

nounExplanation
cdeclA definition of an abstract C type(actually, is a lua string)
ctypeC type object
cdataC data object
ctC type format, is a template object, may be cdecl, cdata, ctype
cbcallback object
VLAAn array of variable length
VLSA structure of variable length

ffi.* API

功能: Lua ffi 庫的 API,與 LuaJIT 不可分割。

毫無疑問,在 ?lua ?文件中使用 ?ffi ?庫的時候,必須要有下面的一行。

local ffi = require "ffi"

ffi.cdef

語法: 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.

ffi.typeof

語法: 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[?]")

ffi.new

語法: 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

語法: 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)

ffi.cast

語法: 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ù)字

cdata 對象的垃圾回收

所有由顯式的 ?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ù)

真的很用容易去調(diào)用一個外部 C 庫函數(shù),示例代碼:

local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")

以上操作步驟,如下:

  1. 加載 FFI 庫。
  2. 為函數(shù)增加一個函數(shù)聲明。這個包含在 中括號 對之間的部分,是標準 C 語法。
  3. 調(diào)用命名的 C 函數(shù)——非常簡單。

事實上,背后的實現(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ù)并不包括原始字符串的長度,所以我們要顯式地傳遞進去。

使用 C 數(shù)據(jù)結(jié)構(gòu)

?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)系
IdiomC codeLua code
Pointer dereferencex = *px = p[0]
int *p*p = yp[0] = y
Pointer indexingx = p[i]x = p[i]
int i, *pp[i+1] = yp[i+1] = y
Array indexingx = a[i]x = a[i]
int i, a[]a[i+1] = ya[i+1] = y
struct/union dereferencex = s.fieldx = s.field
struct foo ss.field = ys.field = y
struct/union pointer derefx = sp->fieldx = sp.field
struct foo *spsp->field = ys.field = y
int i, *py = p - iy = p - i
Pointer dereferencex = p1 - p2x = p1 - p2
Array element pointerx = &a[i]x = a + i

小心內(nè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)存泄露多半會在前方等你。寫代碼的時候切記這一點。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號