LuaJIT 之 FFI


1. FFI 教程

原文: FFI Tutorial
相關鏈接:OpenResty 最佳實踐之 FFI

加載 FFI 庫

FFI 庫時默認編譯進 LuaJIT 中的,但是不會默認加載或初始化。因此,當需要使用 FFI 庫時,需要在 Lua 文件的開頭添加如下語句:

local ffi = require("ffi")

訪問標准系統函數

如下示例顯示了如何訪問標准系統函數。

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")

訪問 zlib 壓縮庫

如下示例顯示了如果在 Lua 代碼中訪問 zlib 壓縮庫。

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)

為 C Type 定義 Metamethods

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 相互轉化

如下列表顯示了如何將常見的 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(...)。注意,不需要將其復制到函數范圍的本地變量中。

2. ffi.* API

詞匯表

  • cdecl:抽象 C 類型聲明(Lua 字符串)。
  • ctype:C 類型對象。由 ffi.typeof() 返回的一種特殊的 cdata,當被調用時是作為 cdata 的構造函數。
  • ct:一種類型規范,可用於大多數 API 函數。cdecl,ctype 或 cdata 作為模板類型。
  • cb:一個回調對象。這是一個包含特殊函數指針的 C 數據對象。從 C 代碼調用此函數會運行關聯的 Lua 函數。
  • VLA:通過 [?] 代替元素個數值聲明的一個可變長度數組,如 "int[?]"。當創建的時候必須給出元素個數。
  • VLS:可變長度結構體是一個 C 類型的結構體,最后一個元素是 VLA。適用於聲明和創建的相同規則。

2.1 聲明和訪問外部符號

必須首先聲明外部符號,然后可以通過索引 C 庫命名空間來訪問外部符號,該命名空間自動將符號綁定到特定庫。

2.1.1 ffi.cdef(def)

聲明 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.

2.1.2 ffi.C

這是默認的 C 庫命名空間--注意為大寫的 C。它綁定到目標系統上的默認符號集或庫。這些或多或少與 C 編譯器默認提供的相同,而不指定額外的鏈接庫。

在 POSIX 系統中,它綁定到默認或全局命名空間中的符號。這包括可執行文件中的所有導出符號以及加載到全局命名空間中的任意庫。這至少包括 libc,libm,libdl(在 Linux 中),libgcc(如果使用 GCC 編譯器),以及 LuaJIT 本身提供的 Lua/C API 中的任何導出符號。

2.1.3 clib = ffi.load(name [, global])

這將加載由 name 指定的動態庫,並返回一個綁定到其符號的新 C 庫命名空間。在 POSIX 系統中,如果 global 為 true,這個庫的符號將會加載到全局命名空間中。

如果 name 是路徑,該庫將會從該路徑中加載。否則,name 將以與系統相關的方式進行規范化,並按默認搜索路徑來搜索動態庫:在 POSIX 系統上,如果 name 不包含 '.',則追加擴展名 .so。此外,如果需要,還會添加庫的前綴。所以 ffi.load("z") 在默認的共享庫路徑中搜索 "libz.so"。

2.2 創建 cdata 對象

2.2.1 ffi.typeof

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

2.2.2 ffi.new

如下 API 函數創建 cdata 對象(ctype() 返回 "cdata")。所有創建的對象都是垃圾回收的。

cdata = ffi.new(ct [,nelem] [,init...])
cdata = ctype([nelem,] [init...])

ffi.new 開辟空間,第一個參數為 ctype 對象,ctype 對象最好通過 ctype = ffi.typeof(ct) 構建。

ffi.new 和 ffi.C.malloc 的區別?

如果使用 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)

2.2.3 ffi.cast

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)   -- 轉換為數字

2.2.4 ffi.metatype

ctype = ffi.metatype(ct, metatable)

為給定的 ct 創建一個 ctype 對象,並將其與 metatable 相關聯。僅允許使用 struct/union 類型,復數和向量。如果需要,其他類型可以封裝在 struct 中。

與 metatable 的關聯是永久性的,之后不可更改。之后,metatable 的內容和 __index 表(如果有的話)的內容都不能被修改。無論對象如何創建或源自何處,相關地元表都會自動應用於此類型的所有用途。注意,對類型的預定義操作具有優先權(如,聲明的字段名稱不能被覆蓋)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM