Lua FFI 實戰

工程師WWW發表於2013-11-22

May 19, 2013

由來

FFI庫,是LuaJIT中最重要的一個擴充套件庫。它允許從純Lua程式碼呼叫外部C函式,使用C資料結構。有了它,就不用再像Lua標準math庫一樣,編寫Lua擴充套件庫。把開發者從開發Lua擴充套件C庫(語言/功能繫結庫)的繁重工作中釋放出來。

FFI簡介

FFI庫,允許從純Lua程式碼呼叫外部C函式,使用C資料結構。

FFI庫最大限度的省去了使用C手工編寫繁重的Lua/C繫結的需要。不需要學習一門獨立/額外的繫結語言——它解析普通C宣告。這樣可以從C標頭檔案或參考手冊中,直接剪下,貼上。它的任務就是繫結很大的庫,但不需要搗鼓脆弱的繫結生成器

FFI緊緊的整合進了LuaJIT(幾乎不可能作為一個獨立的模組)。JIT編譯器為Lua程式碼直接訪問C資料結構而產生的程式碼,等同於一個C編譯器應該生產的程式碼。在JIT編譯過的程式碼中,呼叫C函式,可以被內連處理,不同於基於Lua/C API函式呼叫。

這一頁將簡要介紹FFI庫的使用方法。

激勵範例:呼叫外部C函式

真的很用容易去呼叫一個外部C庫函式:

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

以上操作步驟,如下:

① 載入FFI庫
② 為函式增加一個函式宣告。這個包含在`中括號`對之間的部分,是標準C語法。.
③ 呼叫命名的C函式——非常簡單

事實上,背後的實現遠非如此簡單:③ 使用標準C庫的名稱空間ffi.C。通過符號名("printf")索引這個名稱空間,自動繫結標準C庫。索引結果是一個特殊型別的物件,當被呼叫時,執行printf函式。傳遞給這個函式的引數,從Lua物件自動轉換為相應的C型別。

Ok,使用printf()不是一個壯觀的示例。你也可能使用了io.write()string.format()。但你有這個想法…… 以下是一個Windows平臺彈出訊息框的示例:

  1. local ffi = require("ffi")
  2. ffi.cdef[[
  3. int MessageBoxA(void *w, const char *txt, const char *cap, int type);
  4. ]]
  5. ffi.C.MessageBoxA(nil, "Hello world!", "Test", 0)

Bing! 再一次, 遠非如此簡單,不?

和要求使用Lua/C API去繫結函式的努力相比:

  • 建立一個外部C檔案,
  • 增加一個C函式,遍歷和檢查Lua傳遞的引數,並呼叫這個真實的函式,

傳統的處理方式

  • 增加一個模組函式列表和對應的名字,
  • 增加一個luaopen_*函式,並註冊所有模組函式,
  • 編譯並連結為一個動態庫(DLL),
  • 並將庫檔案遷移到正確的路徑,
  • 編寫Lua程式碼,載入模組
  • 等等……
  • 最後呼叫繫結函式。

唷!(很不爽呀!)

激勵示例: 使用C資料結構

FFI庫允許你建立,並訪問C資料結構。當然,其主要應用是C函式介面。但,也可以獨立使用。

Lua構建在高階資料型別之上。它們很靈活、可擴充套件,而且是動態的。這就是我們大家都喜歡Lua的原因所在。唉,針對特殊任務,你需要一個低階的資料結構時,這可能會低效。例如,一個超大的不同結構的陣列,需要通過一張超大的表,儲存非常多的小表來實現。這需要大量的記憶體開銷以及效能開銷。

這裡是一個庫的草圖,操作一個彩圖,以及一個基準。首先,樸素的Lua版本,如下:

  1. local floor = math.floor
  2.  
  3. local function image_ramp_green(n)
  4. local img = {}
  5. local f = 255/(n-1)
  6. for i=1,n do
  7. img[i] = { red = 0, green = floor((i-1)*f), blue = 0, alpha = 255 }
  8. end
  9. return img
  10. end
  11.  
  12. local function image_to_grey(img, n)
  13. for i=1,n do
  14. local y = floor(0.3*img[i].red + 0.59*img[i].green + 0.11*img[i].blue)
  15. img[i].red = y; img[i].green = y; img[i].blue = y
  16. end
  17. end
  18.  
  19. local N = 400*400
  20. local img = image_ramp_green(N)
  21. for i=1,1000 do
  22. image_to_grey(img, N)
  23. end

以上程式碼,建立一個160.000畫素的一張表,其中每個元素是一張持有4個範圍0至255的數字值的表。首先,建立了一張綠色斜坡的圖(1D,為了簡單化),然後進行1000次灰階轉換操作。實在很蠢蛋,可是我需要一個簡單示例……

以下是FFI版本程式碼。其中,被修改的部分加粗標註:

① local ffi = require("ffi")
ffi.cdef[[
typedef struct { uint8_t red, green, blue, alpha; } rgba_pixel;
]]

② local function image_ramp_green(n)
  local img = ffi.new("rgba_pixel[?]", n)
  local f = 255/(n-1)
③  for i=0,n-1 do
④  img[i].green = i*f
    img[i].alpha = 255
  end
  return img
end

local function image_to_grey(img, n)
③ for i=0,n-1 do
⑤   local y = 0.3*img[i].red + 0.59*img[i].green + 0.11*img[i].blue
    img[i].red = y; img[i].green = y; img[i].blue = y
  end
end

local N = 400*400
local img = image_ramp_green(N)
for i=1,1000 do
  image_to_grey(img, N)
end

Ok, 這是不是太困難:

① 首先,載入FFI庫,宣告底層資料型別。這裡我們選擇一個資料結構,持有4位元組欄位,每一個由4x8 RGBA畫素組成。

② 通過ffi.new()直接建立這個資料結構——其中'?'是一個佔位符,變長陣列元素個數

③ C資料是基於0的(zero-based),所以索引必須是0 到 n-1。你可能需要分配更多的元素,而不僅簡化轉換一流程式碼。

④ 由於ffi.new()預設0填充(zero-fills)陣列, 我們僅需要設定綠色和alpha欄位。

⑤ 呼叫math.floor()的過程可以省略,因為轉換為整數時,浮點數已經被向0截斷。這個過程隱式的發生在資料被儲存在每一個畫素的欄位時。

現在讓我們看一下主要影響的變更:

首先,記憶體消耗從22M降到640K(4004004位元組)。少了35x。所以,表確實有一個顯著的開銷。BTW(By the Way: 順便說一句): 原始Lua程式在x64平臺應該消耗40M記憶體。

其次,效能:純Lua版本執行耗時9.57秒(使用Lua解析器52.9秒),而FFI版本在我的主機上耗時0.48秒(YMMV: 因人而異)。快了20x(比Lua解析器快了110x`)。

狂熱的讀者,可能注意到了為顏色將純Lua程式碼版本轉為使用陣列索引([1] 替換 .red, [2] 替換 .green 等)應該更加緊湊和更快。這個千真萬確(大約1.7x)。從結構切換到陣列也會有幫助。

雖然最終的程式碼不是慣用的,而容易出錯。它仍然沒有得到甚至接近FFI版本程式碼的效能。同時,高階資料結構不容易傳遞給別的C函式,尤其是I/O函式,沒有過分轉換處罰。

待續

擴充套件閱讀

安裝LuaJIT

mkdir -p ~/lua-ffi_in_action && cd ~/lua-ffi_in_action
git clone http://luajit.org/git/luajit-2.0.git
cd luajit-2.0
make && make install

祝大家玩的開心

相關文章