FFIチュートリアル
このページは、いくつかの使用例とガイドラインを提示することによって、FFIライブラリの機能の概要を提供することを目的としています。
ただし、このページではFFIライブラリのすべてを説明しようとはしていません。より詳しく学ぶためには、ffi.* API関数リファレンスやFFIの意味論を見てみることをお勧めします。
FFIライブラリのロード
FFIライブラリはデフォルトでLuaJITに組み込まれていますが、デフォルトではロードおよび初期化されません。FFIライブラリを使用する推奨される方法は、その関数を必要とする各Luaファイルの先頭に次のように追加することです:
local ffi = require("ffi")
この動作はグローバル変数テーブルにffi変数を定義しないことに注意してください — 実際にはローカル変数を使用する必要があります。require関数はライブラリが一度だけロードされることを保証します。
注記
コマンドライン実行可能ファイルのインタラクティブプロンプトからFFIを試す場合は、ローカル変数は行を超えて保持されないため、localを省略します。
標準システム関数へのアクセス
以下のコードは、標準システム関数へのアクセス方法を説明しています。点を出力した後に10ミリ秒間スリープすることで、ゆっくりと2行の点を印刷します:
local ffi = require("ffi")
-- ①
ffi.cdef[[
void Sleep(int ms);
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
]]
local sleep
-- ②
if ffi.os == "Windows" then
-- ③
function sleep(s)
-- ④
ffi.C.Sleep(s*1000)
end
else
function sleep(s)
-- ⑤
ffi.C.poll(nil, 0, s*1000)
end
end
for i=1,160 do
io.write("."); io.flush()
-- ⑥
sleep(0.01)
end
io.write("\n")
手順に沿った説明は以下の通りです:
- ① これは私たちが使用しようとしているCライブラリ関数を定義します。二重括弧内の部分(緑色で示されている)は標準的なC構文です。通常、この情報はCのヘッダーファイルや各CライブラリまたはCコンパイラによって提供されるドキュメントから取得できます。
- ② ここで直面している難しさは、選択する標準が異なるということです。WindowsにはシンプルなSleep()関数があります。他のシステムでは、1秒未満のスリープを実現するためのさまざまな関数が利用可能ですが、明確な合意はありません。幸いにも、poll()もこのタスクに使用でき、ほとんどの非Windowsシステムに存在します。ffi.osのチェックにより、Windows固有の関数をWindowsシステム上でのみ使用するようにしています。
- ③ ここでは、C関数への呼び出しをLua関数内でラップしています。これは厳密には必要ではありませんが、システム固有の問題をコードの一部分でのみ扱うのに役立ちます。このラップ方法により、OSのチェックは初期化時にのみ行われ、各呼び出し時には行われないことが保証されます。
- ④ より微妙な点は、この例のために私たちが定義したsleep()関数が秒数を取るとしていますが、小数点以下の秒も受け入れるということです。これを1000倍するとミリ秒になりますが、それでもLuaの数値は浮動小数点値のままです。しかし、Sleep()関数は整数値のみを受け入れます。幸運なことに、FFIライブラリは関数を呼び出す際に自動的に変換を行います(C言語のように、浮動小数点値をゼロ方向に切り捨てます)。
INFO
一部の読者は、Sleep()がKERNEL32.DLLの一部であり、stdcall関数でもあることに気付くでしょう。これがどうして可能なのでしょうか?FFIライブラリは、ffi.CというデフォルトのCライブラリ名前空間を提供しており、Cコンパイラがそうするように、デフォルトのライブラリセットから関数を呼び出すことを可能にします。また、FFIライブラリは自動的にstdcall関数を検出するので、そのように宣言する必要はありません。
- ⑤ poll()関数は使用しないいくつかの追加の引数を取ります。NULLポインターを渡すために単にnilを使用し、nfdsパラメーターには0を使用できます。なお、C++とは異なり、数値0はポインター値に変換されないことに注意してください。ポインター引数には実際にポインターを、数値引数には数値を渡す必要があります。
INFO
FFIセマンティクスに関するページには、LuaオブジェクトとCタイプの間の変換に関する詳細な情報があります。ほとんどの場合、これを自分で扱う必要はありません。これは自動的に実行され、LuaとCの間の意味的な違いを橋渡しするように慎重に設計されています。
- ⑥ 独自のsleep()関数を定義したので、プレーンなLuaコードからそれを呼び出すことができます。それほど悪くなかったですね?これらの退屈なアニメーションドットを魅力的なベストセラーゲームに変えることは、読者にとっての課題として残されています。😃
zlib圧縮ライブラリへのアクセス
以下のコードは、Luaコードからzlib圧縮ライブラリにアクセスする方法を示しています。文字列を取り、それを別の文字列に圧縮または解凍する2つの便利なラッパー関数を定義します:
local ffi = require("ffi")
-- ①
ffi.cdef[[
unsigned long compressBound(unsigned long sourceLen);
int compress2(uint8_t *dest, unsigned long *destLen,
const uint8_t *source, unsigned long sourceLen, int level);
int uncompress(uint8_t *dest, unsigned long *destLen,
const uint8_t *source, unsigned long sourceLen);
]]
-- ②
local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z")
local function compress(txt)
-- ③
local n = zlib.compressBound(#txt)
local buf = ffi.new("uint8_t[?]", n)
-- ④
local buflen = ffi.new("unsigned long[1]", n)
local res = zlib.compress2(buf, buflen, txt, #txt, 9)
assert(res == 0)
-- ⑤
return ffi.string(buf, buflen[0])
end
-- ⑥
local function uncompress(comp, n)
local buf = ffi.new("uint8_t[?]", n)
local buflen = ffi.new("unsigned long[1]", n)
local res = zlib.uncompress(buf, buflen, comp, #comp)
assert(res == 0)
return ffi.string(buf, buflen[0])
end
-- ⑦
-- Simple test code.
local txt = string.rep("abcd", 1000)
print("Uncompressed size: ", #txt)
local c = compress(txt)
print("Compressed size: ", #c)
local txt2 = uncompress(c, #txt)
assert(txt2 == txt)
こちらがステップバイステップの説明です:
- ① これは、zlibによって提供されるいくつかのC関数を定義します。この例のために、いくつかの型の間接指示が簡略化されており、zlib API/ABIに準拠しつつ、事前定義された固定サイズの整数型を使用しています。
- ② これはzlib共有ライブラリをロードします。POSIXシステムでは、それはlibz.soと名付けられ、通常はプリインストールされています。ffi.load()は任意の欠けている標準の接頭辞/接尾辞を自動的に追加するため、単純に"z"ライブラリをロードすることができます。Windowsではzlib1.dllと名付けられており、zlibのサイトから最初にダウンロードする必要があります。ffi.osのチェックにより、ffi.load()に正しい名前を渡すことを確認します。
- ③ 最初に、圧縮バッファの最大サイズは、圧縮されていない文字列の長さでzlib.compressBound関数を呼び出すことによって取得されます。次の行では、このサイズのバイトバッファが割り当てられます。型指定の
[?]
は、可変長配列(VLA)を示しています。この配列の実際の要素数は、ffi.new()への2番目の引数として与えられます。 - ④ 最初は奇妙に見えるかもしれませんが、zlibのcompress2関数の宣言を見てみてください:宛先の長さはポインタとして定義されています!これは、最大バッファサイズを渡し、使用された実際の長さを取得するためです。 Cでは、ローカル変数のアドレス(&buflen)を渡します。しかし、Luaにはアドレス演算子がないため、単一要素の配列を渡すだけです。便利なことに、一歩で最大バッファサイズで初期化することができます。その後、実際のzlib.compress2関数を呼び出すのは簡単です。
- ⑤ 圧縮されたデータをLuaの文字列として返したいので、ffi.string()を使用します。これには、データの開始位置へのポインタと実際の長さが必要です。長さはbuflen配列で返されるので、そこから取得します。
注意
関数が今戻るため、bufとbuflen変数は最終的にガーベジコレクションの対象になります。これは問題ありません。なぜなら、ffi.string()は内容を新しく作成された(インターンされた)Lua文字列にコピーしているからです。この関数を何度も呼び出す予定がある場合は、バッファを再利用するか、または結果を文字列ではなくバッファで返すことを検討してください。これにより、ガーベジコレクションと文字列のインターニングのオーバーヘッドが削減されます。
- ⑥ uncompress関数は、compress関数の正反対の操作を行います。圧縮データには元の文字列のサイズが含まれていないため、これを渡す必要があります。それ以外には、ここで驚くべきことはありません。
- ⑦ 私たちが定義した関数を使用するコードは、単なるプレーンなLuaコードです。LuaJIT FFIについて知る必要はありません。便利なラッパー関数がそれを完全に隠しています。 LuaJIT FFIの大きな利点の一つは、これらのラッパーをLuaで書くことができるようになったことです。そして、Lua/C APIを使用して別のCモジュールを作成するのにかかる時間のごく一部です。多くの単純なC関数は、ラッパーなしで直接Luaコードから使用できるかもしれません。
補足
zlib APIは、長さとサイズを渡すためにlong型を使用します。しかし、それらのzlib関数は実際には32ビット値のみを扱います。これは公開APIにとって不運な選択ですが、zlibの歴史によって説明されるかもしれません。私たちはそれに対処する必要があります。
最初に、longはたとえばPOSIX/x64システムでは64ビット型ですが、Windows/x64および32ビットシステムでは32ビット型です。したがって、long結果はターゲットシステムに依存して、プレーンなLua数値またはボックス化された64ビット整数cdataオブジェクトのいずれかになります。
OK、ですので、ffi.*関数は一般に、数値を使用したい場所であればどこでもcdataオブジェクトを受け入れます。それが私たちが上でnをffi.string()に渡すのを可能にしています。しかし、他のLuaライブラリ関数やモジュールはこれを扱う方法を知りません。従って、最大限の移植性を得るためには、long結果を他に渡す前にtonumber()を使用する必要があります。そうでないと、アプリケーションはいくつかのシステムで動作するかもしれませんが、POSIX/x64環境では失敗するでしょう。
C型にメタメソッドを定義する
以下のコードは、C型にメタメソッドを定義する方法を説明しています。単純なポイント型を定義し、いくつかの操作を追加します:
local ffi = require("ffi")
-- ①
ffi.cdef[[
typedef struct { double x, y; } point_t;
]]
-- ②
local point
local mt = {
-- ③
__add = function(a, b) return point(a.x+b.x, a.y+b.y) end,
__len = function(a) return math.sqrt(a.x*a.x + a.y*a.y) end,
-- ④
__index = {
area = function(a) return a.x*a.x + a.y*a.y end,
},
}
-- ⑤
point = ffi.metatype("point_t", mt)
-- ⑥
local a = point(3, 4)
print(a.x, a.y) --> 3 4
print(#a) --> 5
print(a:area()) --> 25
local b = a + point(0.5, 8)
print(#b) --> 12.5
ステップバイステップの説明は以下の通りです:
- ① これは、二次元のポイントオブジェクトのC型を定義します。
- ② メタメソッド内で使用されるため、最初にポイントコンストラクタを保持する変数を宣言する必要があります。
- ③ 2つのポイントの座標を加算して新しいポイントオブジェクトを作成する__addメタメソッドを定義しましょう。単純化のために、この関数は両方の引数がポイントであると仮定しています。しかし、少なくとも一方のオペランドが必要なタイプであれば(例えば、ポイントに数を加える、またはその逆)、オブジェクトの任意の組み合わせが可能です。__lenメタメソッドは、ポイントから原点までの距離を返します。
- ④ 演算子が不足した場合、名前付きメソッドも定義できます。ここでは、__indexテーブルが面積関数を定義しています。カスタムインデックスが必要な場合は、__indexと__newindex関数を定義することが望ましいかもしれません。
- ⑤ これにより、メタメソッドがC型に関連付けられます。これは一度だけ行う必要があります。便宜上、コンストラクタはffi.metatype()によって返されます。しかし、それを使用する必要はありません。元のC型は、例えばポイントの配列を作成するために依然として使用できます。メタメソッドは、この型の使用に自動的に適用されます。 メタテーブルとの関連付けは永続的であり、メタテーブルは後から変更してはいけません!__indexテーブルについても同様です。
- ⑥ ここにポイント型の簡単な使用例とその予想結果があります。定義済みの操作(a.xなど)は、新たに定義されたメタメソッドと自由に混合できます。areaはメソッドであり、Luaのメソッド用構文で呼び出す必要があります:a:area()、a.area()ではありません。
C型のメタメソッドメカニズムは、オブジェクト指向スタイルで書かれたCライブラリと組み合わせて使用する場合に最も有用です。クリエイターは新しいインスタンスへのポインタを返し、メソッドは最初の引数としてインスタンスポインタを取ります。時には、__indexをライブラリの名前空間に指定し、__gcをデストラクタに指定するだけで済むこともあります。しかし、実際のLua文字列を返す場合や複数の値を返す場合など、便利なラッパーを追加したくなることがよくあります。
一部のCライブラリは、インスタンスポインタを不透明なvoid *型としてのみ宣言します。この場合、すべての宣言に偽の型を使用できます。例えば、名前付き(不完全な)構造体へのポインタであるtypedef struct foo_type *foo_handleが使用できます。C側はLuaJIT FFIで宣言した内容を知りませんが、基礎となる型が互換性がある限り、全てが正常に動作します。
Cの慣用句の翻訳
以下は、一般的なCの慣用句とそれらのLuaJIT FFIへの翻訳のリストです:
慣用句 | Cコード | Luaコード |
---|---|---|
ポインタの参照解除int *p; | x = *p; *p = y; | x = p[0] p[0] = y |
ポインタのインデックス指定int i, *p; | x = p[i]; p[i+1] = y; | x = p[i] p[i+1] = y |
配列のインデックス指定int i, a[]; | x = a[i]; a[i+1] = y; | x = a[i] a[i+1] = y |
struct/unionの参照解除struct foo s; | x = s.field; s.field = y; | x = s.field s.field = y |
struct/unionポインタの参照解除struct foo *sp; | x = sp->field; sp->field = y; | x = s.field s.field = y |
ポインタの算術演算int i, *p; | x = p + i; y = p - i; | x = p + i y = p - i |
ポインタの差int *p1, *p2; | x = p1 - p2; | x = p1 - p2 |
配列要素のポインタint i, a[]; | x = &a[i]; | x = a+i |
ポインタをアドレスにキャストint *p; | x = (intptr_t)p; | x = tonumber(ffi.cast("intptr_t",p)) |
出力引数付き関数void foo(int *inoutlen); | int len = x; foo(&len); y = len; | local len = ffi.new("int[1]", x) foo(len) y = len[0] |
可変引数の変換int printf(char *fmt, ...); | printf("%g", 1.0); printf("%d", 1); | printf("%g", 1); printf("%d",ffi.new("int", 1)) |
キャッシュするかしないか
ローカル変数やアップバリューにライブラリ関数をキャッシュするのは一般的なLuaの慣用句です。例えば:
local byte, char = string.byte, string.char
local function foo(x)
return char(byte(x)+1)
end
これは、複数のハッシュテーブル検索を(より速い)ローカルやアップバリューの直接使用に置き換えます。LuaJITではそれほど重要ではありませんが、JITコンパイラはハッシュテーブルの検索を大幅に最適化し、ほとんどを内部ループの外に移動させることができます。しかし、すべてを排除することはできませんし、頻繁に使用される関数のタイピングを減らすことができます。したがって、LuaJITを使用しても、これにはまだ場所があります。
FFIライブラリを介してC関数を呼び出す場合、状況は少し異なります。JITコンパイラは、Cライブラリの名前空間から解決された関数のすべてのルックアップオーバーヘッドを排除する特別なロジックを持っています!したがって、次のように個々のC関数をキャッシュするのは役に立たず、実際には逆効果です:
local funca, funcb = ffi.C.funca, ffi.C.funcb -- 役に立ちません!
local function foo(x, n)
for i=1,n do funcb(funca(x, i), 1) end
end
これは間接呼び出しに変換され、より大きく遅いマシンコードを生成します。代わりに、名前空間自体をキャッシュし、JITコンパイラがルックアップを排除することを信頼する必要があります:
local C = ffi.C -- 代わりにこれを使用!
local function foo(x, n)
for i=1,n do C.funcb(C.funca(x, i), 1) end
end
これにより、より短く、より速いコードが生成されます。ですから、C関数をキャッシュしないでくださいが、名前空間はキャッシュしてください!ほとんどの場合、名前空間は外部スコープのローカル変数にすでに存在しています(例:local lib = ffi.load(...))。関数スコープ内のローカル変数にコピーすることは不要であることに注意してください。