Lua FFI 實戰
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平臺彈出訊息框的示例:
- local ffi = require("ffi")
- ffi.cdef[[
- int MessageBoxA(void *w, const char *txt, const char *cap, int type);
- ]]
- 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版本,如下:
- local floor = math.floor
- local function image_ramp_green(n)
- local img = {}
- local f = 255/(n-1)
- for i=1,n do
- img[i] = { red = 0, green = floor((i-1)*f), blue = 0, alpha = 255 }
- end
- return img
- end
- local function image_to_grey(img, n)
- for i=1,n do
- local y = floor(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
以上程式碼,建立一個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 FFI Library
- Terra
- LPEG: Parsing Expression Grammars For Lua, version 0.12
- Lua中通過ffi呼叫c的結構體變數
- 使用 luajit 的 ffi 繫結 zeromq
- Playing with LuaJIT FFI
- LuaJIT FFI 呼叫 Curl 示例
- Lua String Templates
- Standalone FFI library for calling C functions from lua
- Lua遊戲開發實踐指南
- Lua程式設計:第2版
- Beginning Lua Programming
安裝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
祝大家玩的開心
相關文章
- lua的FFI庫--alien
- PHP FFI 實現listPHP
- PHP 7.4 前瞻:FFIPHP
- Redis 實戰 —— 14. Redis 的 Lua 指令碼程式設計Redis指令碼程式設計
- node-ffi使用指南
- Nginx使用Lua模組實現WAFNginx
- ngx_lua_websocket server實現WebServer
- Lua 中實現物件導向物件
- lua版promise實現 - 結束Promise
- lua——alien庫實現lua呼叫C動態連結庫(dll、so)
- PHP通過FFI呼叫CJieba分詞PHPJieba分詞
- PHP透過FFI呼叫CJieba分詞PHPJieba分詞
- 整理下electronJs整合ffi踩得坑JS
- Redis 設計與實現 (九)--LuaRedis
- PHP FFI呼叫go,居然比go還快PHPGo
- 在Java中使用panama FFI呼叫Rust庫JavaRust
- Nginx+lua 實現呼叫.so檔案Nginx
- nginx+lua+redis實現灰度釋出NginxRedis
- nginx+lua(OpenResty),實現訪問限制NginxREST
- redis+lua指令碼實現介面限流Redis指令碼
- 實戰:一種在http請求中使用protobuffer+nginx+lua收集打點日誌的方案HTTPNginx
- Lua 特性
- 《Lua-in-ConTeXt》09:學一點 LuaContext
- Lua遊戲開發(一)---Lua語言遊戲開發
- lua~IDEA中除錯lua指令碼Idea除錯指令碼
- Lua設計與實現--讀書筆記筆記
- 在Lua中實現Rust物件的繫結Rust物件
- 【Lua】VSCode 搭建 Lua 開發環境VSCode開發環境
- Redis與Lua及Redis-py應用LuaRedis
- Lua math庫
- 使用Golang + lua實現一個值班機器人Golang機器人
- 深入xLua實現原理之Lua如何呼叫C#C#
- 深入xLua實現原理之C#如何呼叫LuaC#
- Redis使用lua指令碼實現庫存扣減Redis指令碼
- redis+lua實現指令碼一鍵查詢Redis指令碼
- 基於lua協程的AI服務實現AI
- lua版promise實現3 - 條件判斷例子Promise
- 使用Lua語言實現基本影像識別