看了上文以後,各位讀者可能會得出這樣的結論:
雖然 FFI 用起來很方便,可是性能會有些問題,因此仍是要慎用啊。編程
這又是一個 「FFI 方便可是性能不行」 的例子嗎?segmentfault
並非。上文提到,在編譯模式下,LuaJIT FFI 的性能會是解釋模式下的十倍。因此當程序運行於編譯模式時,用
FFI 並不會慢。數組
還有一筆帳值得一算:調用 Lua CFunction 會迫使 LuaJIT 退回到解釋模式,而經過 FFI 調用 C 函數則不會。
因此不能光計算 FFI 的開銷,還要看由於不用 FFI,致使 Lua 代碼沒法被編譯掉的損耗。
在代碼中大量調用 Lua CFunction,會使得 LuaJIT 的 JIT tracing 變得支離破碎。
即便由於 stitch 的緣故,讓剩餘的部分可以被編譯掉,stitch 自己也會帶來些許開銷。安全
這就是爲何 OpenResty 在已經有了一套用 Lua CFunction 實現的 API 的狀況下,還開了 lua-resty-core 這個項目,
用 FFI 把部分 API 從新實現的緣故。另外,OpenResty 大部分新的 API 只提供 lua-resty-core 裏面的 FFI 版本,
而再也不有 Lua CFunction 實現了。編程語言
除了不會打斷 tracing,FFI 實現的版本還有另外一個優點:LuaJIT 可以在編譯時優化 FFI 實現代碼。函數
傳統的 Lua CFunction 是這樣的:宿主註冊一個 CFunction,在這個 CFunction 裏面調用 Lua C API 跟傳進來的 lua_State
交互。因爲它們無法被 JIT tracing,對於 LuaJIT 而言,這些操做處於黑盒當中,無法進行優化。性能
而對於 FFI,交互部分是用 Lua 實現的。這部分代碼能夠被 JIT tracing,並進行優化。這麼一來,就能省去
沒必要要的類型轉換和字符串建立的操做。優化
一個明顯的例子是,lua-resty-core 裏面的 ngx.re.match
實現,要比原來的 CFunction 實現快一倍。
事實上,大部分在 lua-resty-core 從新實現的 API,要比原來的實現更快(即便它們的核心邏輯是共享的),
有的甚至快上數倍。lua
若是你正在使用 OpenResty 開發項目,建議你如今就引入 lua-resty-core。
也許在不久的未來,lua-resty-core 就是個必選項了。線程
在最後的部分,咱們來看下 FFI 中的一些技巧或者說一些須要注意的坑。
這裏面有些例子直接引用自 OpenResty 的相關項目。
大部分編程語言裏面,數組下標從 0 開始。然而 Lua 倒是從 1 開始。當咱們好不容易習慣了 Lua 的特立獨行後,
FFI array 又來了個 180 度轉變。跟 C 同樣,ffi.new
建立的數組下標從 0 開始。
若是程序中須要在 Lua table 和 FFI array 之間交換數據,一不當心就趟到坑裏面去了。
對此,除了寫完代碼以後須要認真 review 一下,好像也沒別的解決辦法了。
爲了表示 C 裏面的 NULL,LuaJIT 引入了一個特殊的 cdata,名爲 cdata:NULL。
cdata:NULL 有些行爲讓人難以想象:
local cdata_null = ffi.new("void*", nil) print(tostring(cdata_null)) -- cdata:NULL -- LuaJIT 設置了 cdata:NULL 的 __eq 方法,讓它跟 nil 相等 if cdata_null == nil then print('cdata:NULL is equal to nil') end -- 但不能違背 Lua 裏面只有 nil 和 false 纔是假值的鐵律 if cdata_null then print('...but it is not nil!') end
不知道你們是怎麼在 Lua 裏面判斷一個函數執行結果是否成功的,我本人經常使用的是 if not data then
這種寫法。
然而遇到返回 NULL 的 FFI 函數,用這種寫法就中計了。必需要用 if data ~= nil then
才行。
在代碼中,最好要把 FFI 函數返回的 cdata:NULL 轉換成標準的 Lua nil,否則調用該函數的人可能一不當心就掉坑了。
若是你的 C 函數接受 const char *
或者等價的 const unsigned char/int8_t/... *
這樣的參數類型,
能夠直接傳遞 Lua string 進去,而無需另外準備一個 ffi.new
申請的數組。舉個例子:
ffi.cdef[[ ngx_http_lua_regex_t * ngx_http_lua_ffi_compile_regex(const unsigned char *pat, size_t pat_len, int flags, int pcre_opts, unsigned char *errstr, size_t errstr_size); ]] local errbuf = get_string_buf(MAX_ERR_MSG_LEN) -- 對於 const unsigned char* pat,咱們能夠直接傳遞 Lua 字符串 regex, -- 而對於非 const 的 errstr,咱們須要額外申請一個 buffer compiled = C.ngx_http_lua_ffi_compile_regex(regex, #regex, flags, pcre_opts, errbuf, MAX_ERR_MSG_LEN)
LuaJIT 會直接傳遞 Lua 字符串對象的地址進去。因爲 Lua 字符串跟 C 同樣,都是以 '0' 結尾的,
你能夠像讀取 C 字符串同樣使用傳進來的這一個 const 字符串。固然因爲 strlen
的複雜度是 O(n) 的,
出於性能考慮,通常會在 Lua 層次上獲取字符串長度,而後做爲一個參數傳遞進去。
編寫高性能的 LuaJIT 代碼,有兩個基本點:
lua-resty-core 裏面就應用了一個小技巧,能夠複用 ffi.new
建立的 buffer。
鑑於 lua_State
不是線程安全的,咱們能夠假設一個 lua_State
不會被兩個線程同時調用到。同時絕大部分 FFI 調用的函數裏面都不會 yield。
(你固然能夠用 FFI 來調用,會 yield 某個 lua_State
的 C 函數,不過這並不違反「絕大部分」這一前提)
在以上兩點的保證下,咱們能夠設置一個全局的 buffer,凡是須要臨時 buffer 的 FFI 調用均可以從這個全局的 buffer 裏面申請空間。
這裏是 lua-resty-core 裏面,base.get_string_buf
的實現:
local str_buf_size = 4096 local str_buf local c_buf_type = ffi.typeof("char[?]") function _M.get_string_buf(size, must_alloc) -- ngx.log(ngx.ERR, "str buf size: ", str_buf_size) if size > str_buf_size or must_alloc then return ffi_new(c_buf_type, size) end if not str_buf then str_buf = ffi_new(c_buf_type, str_buf_size) end return str_buf end
用法:
-- regex.lua local errbuf = get_string_buf(MAX_ERR_MSG_LEN) compiled = C.ngx_http_lua_ffi_compile_regex(regex, #regex, flags, pcre_opts, errbuf, MAX_ERR_MSG_LEN)
考慮到 ffi.cast
把一個 cdata 轉換成另外一個 cdata 時,不會出現額外的內存分配,咱們甚至能夠
把這個全局 buffer 看成其餘 cdata 使用,像這樣:
-- response.lua local ffi_str_type = ffi.typeof("ngx_http_lua_ffi_str_t*") local ffi_str_size = ffi.sizeof("ngx_http_lua_ffi_str_t") mvals_len = #value buf = get_string_buf(ffi_str_size * mvals_len) mvals = ffi_cast(ffi_str_type, buf)
當一個 struct
被屢次使用 ffi.cdef
定義時,LuaJIT 會拋出 "attempt to redefine" 異常。
若是這個結構體來自於 Nginx 或者一些常見第三庫,不免會出現它在不一樣的文件裏被重複定義的狀況。
這時候能夠應用一個小技巧,檢查某個結構體是否已經被定義了:
if not pcall(ffi.typeof, "ngx_str_t") then ffi.cdef[[ typedef struct { size_t len; const unsigned char *data; } ngx_str_t; ]] end
上述代碼中,只有在找不到 ngx_str_t
類型時咱們纔會去定義 ngx_str_t
。這樣一來,
就不用擔憂會有第三方庫忽然引入 ngx_str_t
類型了。
(不過依然有一個問題。若是第三方庫定義的 XX 類型跟實際的 XX 類型不匹配,就會出現本身的定義是正確的,
可是代碼運行時卻會出錯這種詭異的問題……)
有些時候,咱們須要在 Lua 代碼裏面支持同一 C 庫的不一樣版本。不一樣版本里面,一樣功能的 API 可能有不一樣的名字。
在 C 代碼裏,咱們一般會用 #define
的方式抹平這一差別。然而 ffi.cdef
並不支持 #define
。
好在 ffi.cdef
定義和實際使用是分離的。咱們能夠定義全部的名字,而後根據具體的符號是否存在,
選擇對應的函數。像這樣:
ffi.cdef[[ /* EVP_MD_CTX methods for OpenSSL < 1.1.0 */ EVP_MD_CTX *EVP_MD_CTX_create(void); void EVP_MD_CTX_destroy(EVP_MD_CTX *ctx); /* EVP_MD_CTX methods for OpenSSL >= 1.1.0 */ EVP_MD_CTX *EVP_MD_CTX_new(void); void EVP_MD_CTX_free(EVP_MD_CTX *ctx); ]] local evp_md_ctx_new local evp_md_ctx_free if not pcall(function () return C.EVP_MD_CTX_create end) then evp_md_ctx_new = C.EVP_MD_CTX_new evp_md_ctx_free = C.EVP_MD_CTX_free else evp_md_ctx_new = C.EVP_MD_CTX_create evp_md_ctx_free = C.EVP_MD_CTX_destroy end
固然也能夠考慮寫多一個 C 庫做爲中間層,封裝不一樣版本上的差別。
常常會有這種狀況,咱們須要經過一個 C 函數獲取在 C 層次上分配的資源(好比內存),而後
調用另外一個 C 函數釋放這一資源。通常的作法是,使用 ffi.gc
給這一資源註冊對應的 GC
handler,保證該資源必定會被釋放。
在這種狀況下,務必在獲取資源後馬上調用 ffi.gc
。
C++ 裏面有一個 RAII 的概念,大致上既是在對象構造時獲取資源,在對象析構時釋放資源。
經過肯定的對象析構時機,實現肯定的資源釋放。一樣的思想能夠應用到 LuaJIT FFI 上。
更況且,Lua 代碼拋異常的機會比 C++ 裏的多多了。假設獲取資源和調用 ffi.gc
間隔着一些代碼,
即便這些代碼裏裏沒有顯式調用 error
,因爲內存分配失敗時,LuaJIT 會拋異常,因此只要它們涉及
到新對象的建立,就有可能會拋異常,致使 ffi.gc
不會被調用到。因此,請務必在成功獲取
資源後,馬上調用 ffi.gc
。
雖然說鎖也是一種在 C 層次上分配的資源,不過用 ffi.gc
並不能很好地處理它。不像 C++ 裏面
的析構函數,LuaJIT 裏面的 GC 調用時沒法預期的。然而解鎖的時機必須是肯定的。
若是不用 ffi.gc
,而是手動調用解鎖函數,則不免會遇到異常拋出時沒法解鎖的問題。
那若是把兩種方法結合起來呢?就像 file:close
同樣,調用者手動調用解鎖函數,一旦異常
拋出時,則依賴 ffi.gc
保證鎖最終能被解除。惋惜的是,「最終仍是可以解鎖」並不能讓人接受。
在鄙人看來,這種兩難處境,除了從設計上就避免在 Lua 代碼裏持有 C 層次上的鎖,沒有別的辦法能夠破解掉。