FFIライブラリ
FFIライブラリを使用すると、純粋なLuaコードから外部のC関数を呼び出したり、Cのデータ構造を使用したりすることができます。
FFIライブラリは、面倒な手動のLua/CバインディングをC言語で書く必要を大幅に軽減します。別のバインディング言語を学ぶ必要はありません — これは平易なC宣言を解析します!これらはCヘッダーファイルやリファレンスマニュアルからコピー&ペーストすることができます。脆弱なバインディングジェネレータを扱う必要なしに、大規模なライブラリをバインドするのに適しています。
FFIライブラリはLuaJITに密接に統合されています(別のモジュールとしては利用できません)。LuaコードからCのデータ構造にアクセスするためにJITコンパイラによって生成されたコードは、Cコンパイラが生成するコードと同等です。C関数への呼び出しは、従来のLua/C APIを介してバインドされた関数への呼び出しとは異なり、JITコンパイルされたコード内でインライン化されることができます。
このページでは、FFIライブラリの使用法について簡単に紹介します。詳細を学ぶには、ナビゲーションバーのFFIサブトピックを使用してください。
動機付けの例:外部C関数の呼び出し
外部Cライブラリ関数を呼び出すのは本当に簡単です:
-- ①
local ffi = require("ffi")
-- ②
ffi.cdef[[
int printf(const char *fmt, ...);
]]
-- ③
ffi.C.printf("Hello %s!", "world")
それでは、この部分を詳しく見ていきましょう:
- ① FFIライブラリをロードします。
- ② 関数のためのC宣言を追加します。二重括弧内の部分(緑色で示されている)は、標準のC構文です。
- ③ 名前付きC関数を呼び出します — はい、それだけです!
INFO
実際には、裏側で起こっていることは決して単純ではありません:③は標準Cライブラリの名前空間ffi.Cを使用します。この名前空間にシンボル名("printf")でインデックスを付けると、自動的に標準Cライブラリにバインドされます。その結果、特別な種類のオブジェクトが生成され、呼び出されるとprintf関数が実行されます。この関数に渡された引数は、自動的にLuaオブジェクトから対応するCタイプに変換されます。
さて、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)
ビンゴ!また、それはあまりにも簡単でしたよね?
INFO
この作業を従来のLua/C APIを使ってその関数をバインドするのに必要な努力と比較してみてください:余分なCファイルを作成し、Luaから渡された引数の型を取得してチェックし、実際のC関数を呼び出すC関数を追加し、モジュール関数とその名前のリストを追加し、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_gray(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_gray(img, N)
end
これにより、0〜255の範囲の4つの数値を保持する160,000ピクセルのテーブルが作成されます。まず、単純化のために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
それほど難しくなかったですね:
- ① 最初に、FFIライブラリをロードし、低レベルのデータタイプを宣言します。ここでは、4x8ビットのRGBAピクセルの各成分に対応する4つのバイトフィールドを持つ構造体を選択します。
- ② ffi.new() を使ってデータ構造を作成するのは簡単です — '?' は可変長配列の要素数のプレースホルダーです。
- ③ Cの配列は0から始まるため、インデックスは0からn-1まで走らせる必要があります。レガシーコードを変換するのを簡単にするために、もう一つ要素を割り当てることも考えられます。
- ④ ffi.new() はデフォルトで配列をゼロフィルするので、緑とアルファフィールドを設定するだけで済みます。
- ⑤ 数値を整数に変換する際には、既に浮動小数点数はゼロに向かって切り捨てられるため、ここでは math.floor() の呼び出しは省略できます。これは数値が各ピクセルのフィールドに格納されるときに暗黙的に行われます。
変更の影響を見てみましょう:まず、画像のメモリ消費量が22メガバイトから640キロバイト(400*400*4
バイト)に減少しました。これは35倍も少ないです!つまり、テーブルには確かに顕著なオーバーヘッドがあります。ちなみに:オリジナルのプログラムは、プレーンLua(x64上)で40メガバイトを消費します。
次に、パフォーマンス:純粋なLuaバージョンは9.57秒で実行され(Luaインタプリターで52.9秒)、FFIバージョンは私のマシンで0.48秒で実行されます(YMMV)。これは20倍速いです(Luaインタプリターに対して110倍速い)。
INFO
熱心な読者なら、純粋なLuaバージョンを配列インデックスを使用して色を変換する(.red
の代わりに[1]
、.green
の代わりに[2]
など)ことで、よりコンパクトで速くなることに気付くかもしれません。これは確かに真実です(約1.7倍の速度向上)。配列の構造体に切り替えると、さらに改善されます。
しかし、その結果として得られるコードは、あまり典型的ではなく、エラーが発生しやすくなります。そして、それでもFFIバージョンのコードのパフォーマンスには遠く及ばない。また、高レベルのデータ構造は、特にI/O関数など、他のC関数に簡単に渡すことができず、不当な変換ペナルティを伴います。