因為聽到了兩聲雷,所以要發兩篇博客。
——魯迅
這篇博客來聊聊LuaJIT FFI裏面ctypes的實現。
FFI全稱是Foreign Function Interface即異世界語言接口,LuaJIT中使用FFI可以調用其他語言編譯的庫。
一個示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
local ffi = require("ffi") local C = ffi.C ffi.cdef [[ typedef unsigned int time_t; typedef unsigned int suseconds_t; typedef struct timeval { time_t tv_sec; suseconds_t tv_usec; } timeval; typedef struct timezone { int tz_minuteswest; int tz_dsttime; } timezone; int gettimeofday(struct timeval *tv, struct timezone *tz); ]] local tv = ffi.new("timeval[1]") local tz = ffi.new("timezone[1]") C.gettimeofday(tv, tz) print(tv[1].tv_sec) print(tz[1].tz_dsttime) print("tv_sec in timeval offset:", ffi.offsetof(tv[1], "tv_sec")) print("tv_usec in timeval offset:", ffi.offsetof(tv[1], "tv_usec")) |
以上示例會輸出
991970
0
tv_sec in timeval offset:0
tv_usec in timeval offset:4
有關FFI的具體語法不想寫了,感興趣的朋友可以看LuaJIT關於FFI的四篇文檔。看完之後可以再看看同事的一個講座。
在上面這個示例中,我們定義了time_t和suseconds_t作為int_t的別名,timeval和timezone兩個結構體,和gettimeofday這個函數的簽名。簡單地說,FFI首先會在當前進程映像中找到gettimeofday的偏移,由於這個C函數在libc中實現,所以一定會在當前映像中找到。根據函數簽名,將輸入值按定義的數據類型長度壓入棧中;然後,當取值時,FFI根據當前平台和架構計算結構體中每個成員的偏移量,然後直接取出內存中對應偏移下對應字長的數據。
雖然ffi.cdef定義時的語法和C一樣,但是FFI並沒有真正編譯它,而只是將它們按規則轉換成偏移量,並且記錄下來。
不論是enum,typedef還是function,LuaJIT FFI都用一種ctypes來表示它。為了方便用戶不用重新定義像uint32_t這樣的類型,LuaJIT自帶了95種初始的ctypes。在這裡推薦一個工具parseback,可以方便查看各個已定義的ctypes的類型。這個工具依賴的是一個沒有文檔的方法ffi.typeinfo:
1 2 3 4 5 |
local pp = require("parseback.parseback") for i=1,95 do print(i, ":", pp.typeinfo(i).c) end |
輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
1:void 2:const void 3:bool 4:const char 5:char 6:unsigned char 7:short 8:unsigned short 9:int 10:unsigned int 11:long 12:unsigned long 13:float 14:double 15:complex float 16:complex double 17:void * 18:const void * 19:const char * 20:const char [0] 21:enum { } 22:typedef void * va_list; 23:typedef void * __builtin_va_list; 24:typedef void * __gnuc_va_list; 25:typedef long ptrdiff_t; 26:typedef unsigned long size_t; 27:typedef int wchar_t; 28:typedef char int8_t; 29:typedef short int16_t; 30:typedef int int32_t; 31:typedef long int64_t; 32:typedef unsigned char uint8_t; 33:typedef unsigned short uint16_t; 34:typedef unsigned int uint32_t; 35:typedef unsigned long uint64_t; 36:typedef long intptr_t; 37:typedef unsigned long uintptr_t; 38:typedef long ssize_t; 39:/* keyword void: 269 */ 40:/* keyword _Bool: 270 */ 41:/* keyword bool: 270 */ 42:/* keyword char: 271 */ 43:/* keyword int: 272 */ 44:/* keyword __int8: 272 */ 45:/* keyword __int16: 272 */ 46:/* keyword __int32: 272 */ 47:/* keyword __int64: 272 */ 48:/* keyword float: 273 */ 49:/* keyword double: 273 */ 50:/* keyword long: 274 */ 51:/* keyword short: 276 */ 52:/* keyword _Complex: 277 */ 53:/* keyword complex: 277 */ 54:/* keyword __complex: 277 */ 55:/* keyword __complex__: 277 */ 56:/* keyword signed: 278 */ 57:/* keyword __signed: 278 */ 58:/* keyword __signed__: 278 */ 59:/* keyword unsigned: 279 */ 60:/* keyword const: 280 */ 61:/* keyword __const: 280 */ 62:/* keyword __const__: 280 */ 63:/* keyword volatile: 281 */ 64:/* keyword __volatile: 281 */ 65:/* keyword __volatile__: 281 */ 66:/* keyword restrict: 282 */ 67:/* keyword __restrict: 282 */ 68:/* keyword __restrict__: 282 */ 69:/* keyword inline: 283 */ 70:/* keyword __inline: 283 */ 71:/* keyword __inline__: 283 */ 72:/* keyword typedef: 284 */ 73:/* keyword extern: 285 */ 74:/* keyword static: 286 */ 75:/* keyword auto: 287 */ 76:/* keyword register: 288 */ 77:/* keyword __extension__: 289 */ 78:/* keyword __attribute: 291 */ 79:/* keyword __attribute__: 291 */ 80:/* keyword asm: 290 */ 81:/* keyword __asm: 290 */ 82:/* keyword __asm__: 290 */ 83:/* keyword __declspec: 292 */ 84:/* keyword __cdecl: 293 */ 85:/* keyword __thiscall: 293 */ 86:/* keyword __fastcall: 293 */ 87:/* keyword __stdcall: 293 */ 88:/* keyword __ptr32: 294 */ 89:/* keyword __ptr64: 294 */ 90:/* keyword struct: 295 */ 91:/* keyword union: 296 */ 92:/* keyword enum: 297 */ 93:/* keyword sizeof: 298 */ 94:/* keyword __alignof: 299 */ 95:/* keyword __alignof__: 299 */ |
可以看到許多關鍵詞也被加入了初始列表中,這是為了防止熊孩子亂用關鍵詞做類型名稱。
parseback工具還能生成一個dot圖來描述ctype,我們來看看之前的gettimeofday是怎麼表示的:
1 2 3 4 5 6 7 8 9 |
local i = 96 while true do if not ffi.typeinfo(i) then i = i-1 break end i = i + 1 end print(pp.dot(i)) |
因為OpenResty也定義了一堆FFI類型,所以我們用一個循環來尋找ctypes表裏面的最後一個類型,然後用以下命令
resty test.lua | dot -Tpng > 1.png
得到這樣的圖:
這個圖好像有點複雜,我們先看看其中id為460的timeval結構體。
首先它的類型是CT_STRUCT,代表它是一個結構體的開始。
它的size是8,FFI通過這個屬性知道當ffi.new(“timeval”)被調用時,需要分配多少內存。
它的cid(child ID)是0,這裡先跳過。
它有一個sid(sibling ID)屬性,值為461,它代表單鏈表的下一個成員。timeval由兩個成員構成,分別是tv_sec和tv_usec;兩個成員都有指為10的cid,對CT_FIELD來說,cid代表這個成員的類型,在這裡它們都是size為4的CT_NUM。LuaJIT不會在類型上區分int,uint,long,它們都是CT_NUM,區別在於size和unsigned標記。如id為10的類型就是一個unsigned int。tv_usec的offset是4,代表它在結構體里的偏移量;當取timeval的tv_usec成員時,FFI跳過4位元組,取它的類型長度也就是4位元組內存。
我們再回到前一張複雜的圖。
gettimeofday是個CT_FUNC類型的ctype,也就是function;當一個ctype是CT_FUNC時,它的cid表示返回值的類型,這個例子里是9,也就是signed int。
對一個CT_FUNC來說,sid代表參數列表的類型。sid值是469,指向了名為tv的CT_FIELD,它代表一個成員名稱;LuaJIT在存儲函數類型時,其實和存儲一個結構體是類似的。id為469的CT_FIELD的cid為468,表示它的類型是468;468是一個指針CT_PTR,指向id是460的timeval結構體CT_STRUCT。連起來就是說,它是一個*timeval。
理解了這個之後,我們來看下面的故事。LuaJIT FFI中,你可以把一個lua函數作為回調函數傳回FFI中,例如:
1 2 3 4 5 6 7 8 9 |
local ffi = require("ffi") ffi.cdef [[ typedef void (*callback)(int param); int function invoke(callback cb, int a); ]] ffi.C.invoke(function(param) -- do something end, 1) |
當然在C里,你也可以:
1 2 3 4 5 6 7 |
typedef void (callback)(int param); int function invoke(callback *cb, int a); void function myCallback(int a) { } invoke(myCallback, 1); |
這樣在FFI里可以寫成:
1 2 3 4 5 6 7 8 9 10 |
local ffi = require("ffi") ffi.cdef [[ typedef void (callback2)(int param); int function invoke(callback2 *cb, int a); ]] local pp = ffi.cast("callback2*", function(param) -- do something end end) ffi.C.invoke(pp, 1) pp:free() |
看起來沒問題對吧,但是我們如果我們多次循環ffi.cast:
1 2 3 4 5 6 7 8 9 |
while true do local pok, pp = pcall(ffi.cast, "cb1*", function() end) if not pok then print(pp) break end if pp then pp:free() end end |
最終會報錯”table overflow”,這個錯誤來自https://github.com/LuaJIT/LuaJIT/blob/1e66d0f9e6698fdee672c40a9a5b4159c9254091/src/lj_ctype.c#L158,因為某種原因存儲ctypes的表被填滿了。它的上限是65535。我們來看看它被什麼填滿了:
是一個匿名指針,指向一個匿名函數,它的返回值是void。毫無疑問我們的ffi.cast(“callback2*”, …)會在每次調用時創建兩個新的ctype。
在LuaJIT的代碼里,ffi.cdef,ffi.new,ffi.cast都會經過同一段C parser的代碼段lj_cparse的cp_decl_intern函數里,因為除了在cdef中定義類型外,我們也可以在new和cast里定義匿名結構體,所以這些函數都能產生新的ctype。
在這個函數里,當一個符號被認定為function,它就會無條件地新建一個新的ctype。在我們的寫法里,typedef void (callback2)(int param);定義了一個名為callback2的「函數類型」,然後在ffi.cast里,cparser解析了callback2,為它創建了一個新的CT_FUNC類型,然後解析了*,新建了一個指針指向新的CT_FUNC。
實際上,LuaJIT確實是可以通過一些額外的標誌位來使ffi.cast不會創建新的函數類型的。CType這個類型可以增加一個新的成員,來記錄自己的id,通過這種辦法,當解析為CT_FUNC時,如果這個id已賦值,則可以直接使用已有的ctype。我嘗試了一個簡單的patch來驗證我的想法,證明了它的可行性;但是沒有跑完整的測試集。
我在issues里提問了這個問題,Mike Pall小哥熱心地回復了我。當然作者選擇目前這種做法確實是更簡潔的;而且本身對「函數類型」的typedef,可以有不同角度的理解。所以以後記得寫回調函數的時候,定義一個正常的「函數指針」就好了。