FFIセマンティクス
このページでは、FFIライブラリとLua及びCコードとの相互作用に関する詳細なセマンティクスについて説明します。
FFIライブラリはCコードとのインターフェースを設計するために作られており、宣言は平易なC構文で書かれているため、可能な限りC言語のセマンティクスに密接に従います。Lua言語のセマンティクスとのスムーズな相互運用のために、いくつかの小さな譲歩が必要です。
このページの内容に圧倒されないでください — これは参照用であり、疑問がある場合には参照する必要があるかもしれません。このページを流し読みするのは問題ありませんが、ほとんどのセマンティクスは期待通りに「ただ動作」します。CまたはC++のバックグラウンドを持つ開発者にとっては、LuaJIT FFIを使用したアプリケーションの記述は直感的であるべきです。
C言語サポート
FFIライブラリには、最小限のメモリフットプリントを持つ組み込みのCパーサーがあります。これはffi.*ライブラリ関数によってC型や外部シンボルを宣言するために使用されます。
その唯一の目的は、Cヘッダーファイルなどに見られるC宣言を解析することです。定数式を評価することはありますが、Cコンパイラではありません。インラインC関数定義の本体は単に無視されます。
また、これは検証するCパーサーではありません。正しく形成されたC宣言を期待し、受け入れますが、不正な宣言を無視するか、かなり一般的なエラーメッセージを表示することを選ぶかもしれません。疑問がある場合は、お気に入りのCコンパイラに対して入力を確認してください。
Cパーサーは、C99言語標準に準拠し、以下の拡張が含まれています:
- 文字および文字列リテラルの'\e'エスケープ。
- C99/C++のブール型、boolまたは_Boolキーワードで宣言。
- complexまたは_Complexキーワードで宣言される複素数。
- 2つの複素数型:complex(別名 complex double)とcomplex float。
- GCCモードまたはvector_size属性で宣言されるベクトル型。
- 構造体/共用体内の名前のない('透明な')構造体/共用体フィールド。
- 不完全なenum宣言は、不完全な構造体宣言と同様に扱われる。
- 構造体/共用体内の名前のないenumフィールド。これは、宣言された定数がグローバル名前空間にも表示されることを除いて、スコープ付きC++ enumに似ています。
- 構造体/共用体内でのスコープ付きstatic const宣言(C++から)。
- ゼロ長配列(
[0]
)、空の構造体/共用体、可変長配列(VLA,[?]
)、および末尾VLAを持つ可変長構造体(VLS)。 - C++の参照型(int &x)。
- '
__
'でのGCCの代替キーワード、例えば__const__
。 - 次の属性を持つGCC
__attribute__
:aligned, packed, mode, vector_size, cdecl, fastcall, stdcall, thiscall。 - GCCの
__extension__
キーワードとGCCの__alignof__
オペレーター。 - 関数宣言のためのGCC
__asm__("symname")
シンボル名リダイレクション。 - 固定長タイプのためのMSVCキーワード:
__int8
,__int16
,__int32
,__int64
。 - MSVC
__cdecl
,__fastcall
,__stdcall
,__thiscall
,__ptr32
,__ptr64
,__declspec(align(n))
および #pragma pack。 - その他のGCC/MSVC固有の属性は無視されます。
以下のC型はCパーサーによって事前定義されています(typedefのようですが、再宣言は無視されます):
- 可変引数処理:
va_list
、__builtin_va_list
、__gnuc_va_list
。 <stddef.h>
から:ptrdiff_t
、size_t
、wchar_t
。<stdint.h>
から:int8_t
、int16_t
、int32_t
、int64_t
、uint8_t
、uint16_t
、uint32_t
、uint64_t
、intptr_t
、uintptr_t
。<unistd.h>
から(POSIX):ssize_t
。
これらの型を、コンパイラ固有の拡張やターゲット依存の標準型よりも優先して使用することを推奨します。例えば、charの符号付き性とlongのサイズは、ターゲットアーキテクチャとプラットフォームABIによって異なります。
以下のCの機能はサポートされていません:
- 宣言には常に型指定子が必要であり、int型にはデフォルトしません。
- 古いスタイルの空の関数宣言(K&R)は許可されていません。すべてのC関数には適切なプロトタイプ宣言が必要です。パラメーターなしで宣言された関数(int foo();)は、C++のように引数を取らない関数として扱われます。
- long double C型は正しく解析されますが、関連する変換、アクセス、または算術演算はサポートされていません。
- ワイド文字列と文字リテラルはサポートされていません。
- 現在実装されていない機能については下記を参照してください。
C型からLuaオブジェクトへの変換規則
これらの変換規則は、C型への読み取りアクセスに適用されます:ポインター、配列、構造体/共用体型のインデックス付け;外部変数または定数値の読み取り;C呼び出しからの戻り値の取得:
入力 | 変換 | 出力 |
---|---|---|
int8_t, int16_t | →符号拡張 int32_t → double | number |
uint8_t, uint16_t | →ゼロ拡張 int32_t → double | number |
int32_t, uint32_t | → double | number |
int64_t, uint64_t | ボックス化された値 | 64ビット整数cdata |
double, float | → double | number |
bool | 0 → false、それ以外 → true | boolean |
enum | ボックス化された値 | enum cdata |
複素数 | ボックス化された値 | 複素数cdata |
ベクター | ボックス化された値 | ベクターcdata |
ポインター | ボックス化された値 | ポインターcdata |
配列 | ボックス化された参照 | 参照cdata |
構造体/共用体 | ボックス化された参照 | 参照cdata |
ビットフィールドは、基礎となる型のように扱われます。
参照型は、変換が行われる前に参照先のC型に適用される変換が行われる前にデリファレンス(参照解除)されます。
LuaオブジェクトからC型への変換
これらの変換規則は、C型への書き込みアクセスに適用されます:ポインター、配列、構造体/共用体型のインデックス付け;cdataオブジェクトの初期化;C型へのキャスト;外部変数への書き込み;C呼び出しへの引数の渡し:
入力 | 変換 | 出力 |
---|---|---|
number | → | double |
boolean | false → 0, true → 1 | bool |
nil | NULL → | (void *) |
lightuserdata | lightuserdataアドレス → | (void *) |
userdata | userdataペイロード → | (void *) |
io.* ファイル | FILE * ハンドルを取得 → | (void *) |
string | enum定数に一致する | enum |
string | 文字列データ + ゼロバイトのコピー | int8_t[] , uint8_t[] |
string | 文字列データ → | const char[] |
function | コールバックを作成 → | C関数型 |
table | テーブル初期化子 | 配列 |
table | テーブル初期化子 | 構造体/共用体 |
cdata | cdataペイロード → | C型 |
この変換の結果型が目的地のC型と一致しない場合、C型間の変換規則が適用されます。
参照型は初期化後に変更不可です(「参照の再配置は不可」)。初期化目的や参照パラメータに値を渡す際には、ポインターのように扱われます。C++と異なり、Lua言語のセマンティクス下で変数の自動参照生成を実装する方法はありません。参照パラメータを持つ関数を呼び出したい場合、明示的に1要素の配列を渡す必要があります。
C型間の変換
これらの変換規則は、標準のC変換規則とほぼ同じです。一部の規則はキャストにのみ適用されたり、ポインターまたは型の互換性が必要です:
入力 | 変換 | 出力 |
---|---|---|
符号付き整数 | →縮小または符号拡張 | 整数 |
符号なし整数 | →縮小またはゼロ拡張 | 整数 |
整数 | →丸め | double, float |
double, float | →切り捨て int32_t →縮小 | (u)int8_t, (u)int16_t |
double, float | →切り捨て | (u)int32_t, (u)int64_t |
double, float | →丸め | float, double |
数値 | n == 0 → 0, それ以外 → 1 | bool |
bool | false → 0, true → 1 | 数値 |
複素数 | 実部を変換 | 数値 |
数値 | 実部を変換、虚部 = 0 | 複素数 |
複素数 | 実部と虚部を変換 | 複素数 |
数値 | スカラーを変換して複製 | ベクター |
ベクター | コピー(同じサイズ) | ベクター |
構造体/共用体 | ベースアドレスを取得(互換性あり) | ポインター |
配列 | ベースアドレスを取得(互換性あり) | ポインター |
関数 | 関数アドレスを取得 | 関数ポインター |
数値 | uintptr_t経由で変換(キャスト) | ポインター |
ポインター | アドレスを変換(互換性/キャストあり) | ポインター |
ポインター | アドレスを変換(キャスト) | 整数 |
配列 | ベースアドレスを変換(キャスト) | 整数 |
配列 | コピー(互換性あり) | 配列 |
構造体/共用体 | コピー(同一型) | 構造体/共用体 |
ビットフィールドやenum型は、それらの基礎となる型として扱われます。 上記に記載されていない変換はエラーを発生させます。例えば、ポインターを複素数に変換したり、その逆を行うことはできません。
可変引数C関数の引数のための変換
Luaオブジェクトを可変引数C関数の可変引数部分に渡す場合、以下のデフォルト変換規則が適用されます:
入力 | 変換 | 出力 |
---|---|---|
number | → | double |
boolean | false → 0, true → 1 | bool |
nil | NULL → | (void *) |
userdata | userdata payload → | (void *) |
lightuserdata | lightuserdata address → | (void *) |
string | string data → | const char * |
float cdata | → | double |
Array cdata | ベースアドレスを取得 | 要素ポインタ |
struct/union cdata | ベースアドレスを取得 | struct/unionポインタ |
Function cdata | 関数アドレスを取得 | 関数ポインタ |
その他のcdata | 変換なし | C型 |
特定の型としてcdataオブジェクト以外のLuaオブジェクトを渡すには、変換規則をオーバーライドして、コンストラクタまたはキャストで一時的なcdataオブジェクトを作成し、それを渡す値で初期化する必要があります: xがLuaの数値であると仮定して、それを可変引数関数に整数として渡す方法は次のとおりです:
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("integer value: %d\n", ffi.new("int", x))
これを行わない場合、デフォルトのLua number → double 変換規則が適用されます。整数を期待する可変引数C関数は、不正確または初期化されていない値を受け取ることになります。
初期化子
ffi.new() または同等のコンストラクタ構文で cdata オブジェクトを作成すると、その内容も初期化されます。使用されるオプショナル初期化子の数とC型に応じて、異なる規則が適用されます:
- 初期化子が与えられない場合、オブジェクトはゼロバイトで埋められます。
- スカラー型(数値およびポインター)は単一の初期化子を受け入れます。LuaオブジェクトはスカラーC型に変換されます。
- バリアレイ(複素数およびベクトル)は、単一の初期化子が与えられるとスカラーのように扱われます。それ以外の場合は、通常の配列のように扱われます。
- 集合型(配列および構造体)は、同じ型の単一のcdata初期化子(コピーコンストラクタ)、単一のテーブル初期化子、または初期化子のフラットリストのいずれかを受け入れます。
- 配列の要素は、インデックスゼロから初期化されます。配列に単一の初期化子が与えられた場合、それは残りのすべての要素に対して繰り返されます。2つ以上の初期化子が与えられた場合、このことは起こりません:すべての残りの未初期化要素はゼロバイトで埋められます。
- バイト配列はLua文字列で初期化することもできます。これにより、文字列全体と終端のゼロバイトがコピーされます。配列のサイズが既知で固定されている場合のみ、コピーは早期に停止します。
- 構造体のフィールドは、宣言された順序で初期化されます。初期化されていないフィールドはゼロバイトで埋められます。
- 共用体の最初のフィールドのみがフラット初期化子で初期化できます。
- 自体が集合である要素やフィールドは単一の初期化子で初期化されますが、これはテーブル初期化子または互換性のある集合である可能性があります。
- 余分な初期化子はエラーを引き起こします。
テーブル初期化子
Luaテーブルを使用して配列または構造体/共用体を初期化する場合、以下のルールが適用されます:
- テーブルインデックス
[0]
がnilでない場合、テーブルはゼロベースであると見なされます。そうでない場合は、一ベースであると見なされます。 - インデックスゼロから始まる配列要素は、
[0]
または[1]
のどちらかのインデックスから始まる連続するテーブル要素で一つずつ初期化されます。このプロセスは最初のnilテーブル要素で停止します。 - 正確に1つの配列要素が初期化された場合、それは残りのすべての要素に対して繰り返されます。そうでない場合、すべての残りの未初期化要素はゼロバイトで埋められます。
- 上記のロジックは、既知の固定サイズを持つ配列にのみ適用されます。VLAはテーブルで与えられた要素でのみ初期化されます。使用状況に応じて、VLAにNULLまたは0の終端子を明示的に追加する必要があるかもしれません。
- 構造体/共用体は、そのフィールドの宣言順に初期化できます。各フィールドは、
[0]
または[1]
のいずれかのインデックスから始まる連続するテーブル要素で初期化されます。このプロセスは最初のnilテーブル要素で停止します。 - それ以外の場合、インデックス
[0]
も[1]
も存在しない場合、構造体/共用体はテーブル内の各フィールド名(文字列キーとして)をルックアップすることで初期化されます。非nilの値が対応するフィールドを初期化するために使用されます。 - 構造体の未初期化フィールドは、VLSの末尾のVLAを除き、ゼロバイトで埋められます。
- 共用体の初期化は1つのフィールドが初期化された後に停止します。フィールドが初期化されていない場合、共用体はゼロバイトで埋められます。
- 自体が集合である要素やフィールドは単一の初期化子で初期化されますが、これはネストされたテーブル初期化子(または互換性のある集合)である可能性があります。
- 配列の余分な初期化子はエラーを引き起こします。構造体/共用体の余分な初期化子は無視されます。関連しないテーブルエントリも無視されます。
例:
local ffi = require("ffi")
ffi.cdef[[
struct foo { int a, b; };
union bar { int i; double d; };
struct nested { int x; struct foo y; };
]]
ffi.new("int[3]", {}) --> 0, 0, 0
ffi.new("int[3]", {1}) --> 1, 1, 1
ffi.new("int[3]", {1,2}) --> 1, 2, 0
ffi.new("int[3]", {1,2,3}) --> 1, 2, 3
ffi.new("int[3]", {[0]=1}) --> 1, 1, 1
ffi.new("int[3]", {[0]=1,2}) --> 1, 2, 0
ffi.new("int[3]", {[0]=1,2,3}) --> 1, 2, 3
ffi.new("int[3]", {[0]=1,2,3,4}) --> error: too many initializers
ffi.new("struct foo", {}) --> a = 0, b = 0
ffi.new("struct foo", {1}) --> a = 1, b = 0
ffi.new("struct foo", {1,2}) --> a = 1, b = 2
ffi.new("struct foo", {[0]=1,2}) --> a = 1, b = 2
ffi.new("struct foo", {b=2}) --> a = 0, b = 2
ffi.new("struct foo", {a=1,b=2,c=3}) --> a = 1, b = 2 'c' is ignored
ffi.new("union bar", {}) --> i = 0, d = 0.0
ffi.new("union bar", {1}) --> i = 1, d = ?
ffi.new("union bar", {[0]=1,2}) --> i = 1, d = ? '2' is ignored
ffi.new("union bar", {d=2}) --> i = ?, d = 2.0
ffi.new("struct nested", {1,{2,3}}) --> x = 1, y.a = 2, y.b = 3
ffi.new("struct nested", {x=1,y={2,3}}) --> x = 1, y.a = 2, y.b = 3
cdataオブジェクトに対する操作
すべての標準Lua演算子は、cdataオブジェクトまたはcdataオブジェクトと別のLuaオブジェクトの組み合わせに適用できます。以下のリストは、事前定義された操作を示しています。
参照型は、以下の各操作を実行する前に参照解除されます — 操作は参照によって指されるC型に適用されます。
事前定義された操作は、対応するctypeのメタメソッドまたはインデックステーブル(存在する場合)に委譲する前に、常に最初に試みられます(__newを除く)。メタメソッドルックアップまたはインデックステーブルルックアップが失敗した場合はエラーが発生します。
cdataオブジェクトのインデックス付け
- ポインター/配列のインデックス付け:cdataポインター/配列は、cdata数値またはLua数値でインデックス付けできます。要素のアドレスは、基底アドレスに数値値を要素のバイト単位のサイズで乗算したものを加えて計算されます。読み取りアクセスは要素の値をロードし、Luaオブジェクトに変換します。書き込みアクセスはLuaオブジェクトを要素の型に変換し、変換された値を要素に格納します。要素のサイズが未定義である場合、または定数要素への書き込みアクセスが試みられた場合にはエラーが発生します。
- 構造体/共用体フィールドの参照解除:cdata構造体/共用体または構造体/共用体へのポインターは、フィールド名を指定する文字列キーによって参照解除できます。フィールドのアドレスは、基底アドレスにフィールドの相対オフセットを加えたものとして計算されます。読み取りアクセスはフィールドの値をロードし、Luaオブジェクトに変換します。書き込みアクセスはLuaオブジェクトをフィールドの型に変換し、変換された値をフィールドに格納します。定数構造体/共用体または定数フィールドへの書き込みアクセスが試みられた場合にはエラーが発生します。スコープ付き列挙型定数または静的定数は定数フィールドのように扱われます。
- 複素数のインデックス付け:複素数は、0または1の値を持つcdata数値またはLua数値、または文字列"re"または"im"によってインデックス付けできます。読み取りアクセスは複素数の実部(
[0]
、.re)または虚部([1]
、.im)をロードし、Lua数値に変換します。複素数のサブパーツは不変です — 複素数のインデックスに代入するとエラーが発生します。範囲外のインデックスにアクセスすると、未指定の結果が返されますが、メモリアクセス違反を引き起こすことは保証されていません。 - ベクトルのインデックス付け:ベクトルはインデックス付けの目的で配列と同様に扱われますが、ベクトルの要素は不変です — ベクトルのインデックスに代入するとエラーが発生します。
ctypeオブジェクトも文字列キーでインデックスを付けることができます。事前定義された操作は構造体/共用体型のスコープ内定数を読み取ることだけです。他のすべてのアクセスは、対応するメタメソッドやインデックステーブル(存在する場合)に委ねられます。
Note
アドレス演算子が(意図的に)存在しないため、値型を保持するcdataオブジェクトは初期化後に実質的に不変です。JITコンパイラは、特定の最適化を適用する際にこの事実を利用します。
その結果、複素数やベクトルの要素は不変です。しかし、これらの型を保持する集合の要素は、もちろん変更される可能性があります。つまり、foo.c.imに代入することはできませんが、(新しく作成された)複素数をfoo.cに代入することはできます。
JITコンパイラは厳格なエイリアス規則を実装しています:異なる型へのアクセスはエイリアスを形成しませんが、符号付き性の違いに対してはこの限りではありません(これはC99とは異なり、charポインターにも適用されます)。共用体を通じた型のごまかしは明示的に検出され許可されます。
cdataオブジェクトの呼び出し
- コンストラクター:ctypeオブジェクトを呼び出してコンストラクターとして使用できます。これはffi.new(ct, ...)と同等ですが、__newメタメソッドが定義されている場合を除きます。__newメタメソッドは、ctypeオブジェクトとコンストラクターに渡された他の引数を使って呼び出されます。このメタメソッド内ではct(...)を呼び出すと無限再帰を引き起こすため、ffi.newを使用する必要があることに注意してください。
- C関数呼び出し:cdata関数またはcdata関数ポインターを呼び出すことができます。渡された引数は、関数宣言で指定されたパラメーターのC型に変換されます。可変引数C関数の可変引数部分に渡された引数は特別な変換規則を使用します。このC関数が呼び出され、戻り値(存在する場合)はLuaオブジェクトに変換されます。 Windows/x86システムでは、__stdcall関数は自動的に検出され、__cdecl(デフォルト)として宣言された関数は、最初の呼び出し後に自動的に修正されます。
cdataオブジェクトの算術演算
- ポインタ算術:cdataポインタ/配列とcdata数値またはLua数値を加算または減算できます。減算の場合、数値は右側になければなりません。結果は、同じ型のポインタであり、アドレスは数値値に要素サイズ(バイト単位)を乗じたもののプラスまたはマイナスです。要素サイズが未定義の場合はエラーが発生します。
- ポインタ差:互換性のある二つのcdataポインタ/配列を減算できます。結果は、それらのアドレスの差を要素サイズ(バイト単位)で割ったものです。要素サイズが未定義またはゼロの場合はエラーが発生します。
- 64ビット整数算術:標準の算術演算子(+ - * / % ^ および単項マイナス)を二つのcdata数値、またはcdata数値とLua数値に適用できます。片方がuint64_tの場合、他方はuint64_tに変換され、符号なし算術演算が実行されます。それ以外の場合は、両方がint64_tに変換され、符号付き算術演算が実行されます。結果はボックス化された64ビットのcdataオブジェクトです。 オペランドの一方がenumで、もう一方が文字列の場合、文字列は上記の変換前に一致するenum定数の値に変換されます。 これらの規則により、64ビット整数は「粘着性」があります。少なくとも一方のオペランドが64ビット整数である任意の式は、別の64ビット整数を結果とします。除算、剰余、累乗演算子の未定義のケースは2LL ^ 63または2ULL ^ 63を返します。 64ビット整数をLua数値に明示的に変換する必要があります(例:通常の浮動小数点計算のために)tonumber()を使用します。しかし、これには精度損失が発生する可能性があることに注意してください。
- 64ビットビット演算:64ビット算術演算子の規則は類似して適用されます。 他のbit.*演算とは異なり、bit.tobit()はcdata数値をint64_t経由でint32_tに変換し、Lua数値を返します。 bit.band()、bit.bor()、bit.bxor()については、引数のいずれかがcdata数値である場合、すべての引数に対してint64_tまたはuint64_tへの変換が適用されます。 その他の演算については、出力タイプを決定するために最初の引数のみが使用されます。これは、シフトやローテートのシフトカウントとしてcdata数値を受け入れることを意味しますが、それだけではcdata数値の出力にはなりません。
cdataオブジェクトの比較
- ポインタの比較:互換性のある二つのcdataポインタ/配列は比較できます。結果は、それらのアドレスの符号なし比較と同じです。nilはNULLポインタとして扱われ、他の任意のポインタ型と互換性があります。
- 64ビット整数の比較:二つのcdata数値、またはcdata数値とLua数値を互いに比較できます。片方がuint64_tの場合、他方はuint64_tに変換され、符号なし比較が実行されます。それ以外の場合、両方がint64_tに変換され、符号付き比較が実行されます。 オペランドの一方がenumで、もう一方が文字列の場合、文字列は上記の変換前に一致するenum定数の値に変換されます。
- 等価/非等価の比較ではエラーは発生しません。互換性のないポインタでもアドレスによる等価性で比較できます。その他の互換性のない比較(非cdataオブジェクトとの比較も含む)では、二つの側面は不等として扱われます。
cdataオブジェクトをテーブルキーとして
Luaテーブルはcdataオブジェクトでインデックス付けできますが、これは有用な意味を提供しません — cdataオブジェクトはテーブルキーには適していません!
cdataオブジェクトは、他のガベージコレクトされるオブジェクトと同様に扱われ、テーブルインデックスのためにそのアドレスでハッシュ化および比較されます。cdata値型にはインターニングがないため、同じ値が異なるアドレスの異なるcdataオブジェクトでボックス化される可能性があります。したがって、t[1LL+1LL]
とt[2LL]
は通常、同じハッシュスロットを指さず、t[2]
と同じハッシュスロットを指すことは絶対にありません。
値によるハッシュ化と比較のための追加の処理をLuaテーブルに追加すると、実装の複雑さが大幅に増加し、一般的なケースが遅くなるでしょう。VM内での使用が広範囲にわたっていることを考えると、これは受け入れられません。
cdataオブジェクトをキーとして使用する必要がある場合、実行可能な代替手段は3つあります:
- Lua数値の精度(52ビット)で済む場合は、cdata数値にtonumber()を使用するか、cdata集合の複数のフィールドをLua数値に組み合わせます。その結果得られるLua数値をテーブルのインデックスキーとして使用します。 明らかな利点は、
t[tonumber(2LL)]
がt[2]
と同じスロットを指すことです。 - それ以外の場合は、64ビット整数や複素数にtostring()を使用するか、cdata集合の複数のフィールドをLua文字列に組み合わせます(例:ffi.string()を使用)。その結果得られるLua文字列をテーブルのインデックスキーとして使用します。
- Cコードで行うように、FFIライブラリによって提供されるC型を使用して、専用のハッシュテーブル実装を作成します。最終的には、他の代替手段や一般的な値によるハッシュテーブルが提供できるものよりも、はるかに優れたパフォーマンスを得ることができるかもしれません。
パラメータ化された型
いくつかの抽象化を容易にするために、二つの関数ffi.typeofとffi.cdefは、C宣言におけるパラメータ化された型をサポートしています。
Note
cdeclを取る他のAPI関数では、これを許可していません。
typedef名、識別子、または宣言内の数値を記述できる場所では、$(ドル記号)を代わりに記述できます。これらのプレースホルダーは、cdecl文字列に続く引数との出現順に置き換えられます:
-- パラメータ化されたフィールド型と名前を持つ構造体を宣言:
ffi.cdef([[
typedef struct { $ $; } foo_t;
]], type1, name1)
-- 動的名前を持つ匿名構造体:
local bar_t = ffi.typeof("struct { int $, $; }", name1, name2)
-- 派生ポインタ型:
local bar_ptr_t = ffi.typeof("$ *", bar_t)
-- パラメータ化された寸法は、VLAでは機能しない場所でも機能します:
local matrix_t = ffi.typeof("uint8_t[$][$]", width, height)
注意: これは単純なテキスト置換ではありません!渡された ctype または cdata オブジェクトは基礎となる型のように扱われ、渡された文字列は識別子とみなされ、数字は数字とみなされます。この区別を混同してはいけません:例えば、型の代わりに文字列 "int" を渡すことは機能しません、代わりに ffi.typeof("int")
を使用する必要があります。
パラメータ化された型の主な使用例は、C++ テンプレートメタプログラミングで達成できるものに似た、抽象データ型を実装するライブラリ(例)です。別の使用例は、グローバルな struct 名前空間の汚染を避けるための、匿名構造体の派生型です。
パラメータ化された型は優れたツールであり、特定の使用例には不可欠です。しかし、実際のコードでは、すべての型が実際に固定されている場合など、慎重に使用することが望まれます。
cdata オブジェクトのガーベジコレクション
明示的に (ffi.new()
、ffi.cast()
など) または暗黙的に(アクセサを通じて)作成された cdata オブジェクトはガーベジコレクションされます。cdata オブジェクトがまだ使用されている間、Lua スタック、アップバリュー、または Lua テーブルのどこかに有効な参照を保持することが必要です。cdata オブジェクトへの最後の参照がなくなると、ガーベジコレクタは次の GC サイクルの終わりにそれが使用していたメモリを自動的に解放します。
ただし、ポインタ自体も cdata オブジェクトですが、ガーベジコレクタによって追跡されることはありません。したがって、例えば、cdata 配列をポインタに割り当てる場合、ポインタがまだ使用中である限り、配列を保持する cdata オブジェクトを生存させておく必要があります:
ffi.cdef[[
typedef struct { int *a; } foo_t;
]]
local s = ffi.new("foo_t", ffi.new("int[10]")) -- 間違い!
local a = ffi.new("int[10]") -- 正しい
local s = ffi.new("foo_t", a)
-- 's' を何かしらの処理で使用するが、完了するまで 'a' を生存させる。
Luaの文字列にも同様のルールが適用されます。文字列は暗黙的に "const char *" に変換されます:文字列オブジェクト自体はどこかで参照されていなければ、最終的にガーベジコレクションされます。その後、ポインタは古いデータを指すことになり、そのデータはすでに上書きされている可能性があります。文字列リテラルは、それを含む関数(正確にはそのプロトタイプ)がガーベジコレクションされない限り、自動的に生存が保たれることに注意してください。
外部C関数に引数として渡されたオブジェクトは、呼び出しが戻るまで生存が保たれます。したがって、引数リスト内で一時的な cdata オブジェクトを作成することは一般的に安全です。これは、特定のC型をvararg関数に渡すための一般的な慣用句です。
C関数によって返されるメモリ領域(例えば malloc() からのもの)は、もちろん手動で管理する必要があります(または ffi.gc()
を使用します)。cdataオブジェクトへのポインタは、C関数によって返されたポインタと区別がつかないものです(これがGCがそれらを追跡できない理由の一つです)。
コールバック
LuaJIT FFIは、Lua関数がC関数ポインタに変換されるたびに、特別なコールバック関数を自動的に生成します。これにより、生成されたコールバック関数ポインタは、関数ポインタのC型とLua関数オブジェクト(クロージャ)と関連付けられます。
これは、Lua関数を関数ポインタ引数に渡すときなど、通常の変換によって暗黙的に発生する場合があります。または、ffi.cast()
を使用して、Lua関数を明示的にC関数ポインタにキャストすることもできます。
現在、特定のC関数型のみがコールバック関数として使用できます。Cのvararg関数や値渡しの集約引数や結果型を持つ関数はサポートされていません。コールバックから呼び出すことができるLua関数の種類に制限はありません — 適切な引数の数に関するチェックは行われません。Lua関数の戻り値は結果型に変換され、変換が無効であればエラーが投げられます。
コールバックの呼び出しを越えてエラーを投げることは許されますが、一般的にはお勧めできません。コールバックを呼び出したC関数が強制スタックアンワインドを処理し、リソースのリークがない場合にのみこれを行ってください。
許可されていないことの一つは、FFIがC関数を呼び出すことをJITコンパイルさせることです。そのC関数はコールバックを呼び出し、再びLuaに呼び出します。通常、この試みは最初にインタプリタによって捕捉され、C関数はコンパイルのブラックリストに登録されます。
しかし、このヒューリスティックは特定の状況下で失敗することがあります:例えば、メッセージポーリング関数はすぐにLuaコールバックを実行しないかもしれませんが、その呼び出しはJITコンパイルされるかもしれません。後にLuaへのコールバック(例えば、まれに呼び出されるエラーコールバック)が発生すると、「bad callback」というメッセージと共にVM PANICが発生します。その場合、そのようなメッセージポーリング関数(または類似のもの)を呼び出す周囲のLua関数に対して jit.off()
を使って手動でJITコンパイルをオフにする必要があります。
コールバックのリソース処理
コールバックはリソースを消費します — 同時に持てるコールバックの数には限りがあります(アーキテクチャによって500〜1000)。関連付けられたLua関数もガーベジコレクションを防ぐためにアンカーされています。
暗黙の変換によるコールバックは永続的です!そのライフタイムを推測する方法はありません。なぜならC側は後で使用するために関数ポインタを保存するかもしれないからです(GUIツールキットで典型的)。関連するリソースは終了するまで回収できません:
ffi.cdef[[
typedef int (__stdcall *WNDENUMPROC)(void *hwnd, intptr_t l);
int EnumWindows(WNDENUMPROC func, intptr_t l);
]]
-- 関数ポインタ引数を介したコールバックへの暗黙の変換。
local count = 0
ffi.C.EnumWindows(function(hwnd, l)
count = count + 1
return true
end, 0)
-- コールバックは永続的で、そのリソースは回収できません!
-- これを一度だけ行うのであれば、問題ではないかもしれません。
注意
この例は、Windows/x86 システム上で __stdcall コールバックを適切に宣言する必要があることを示しています。__stdcall での Windows 関数への呼び出しとは異なり、呼び出し規約は自動的に検出されません。
特定の使用例では、リソースを解放するか、コールバックを動的にリダイレクトする必要があります。明示的にC関数ポインタへのキャストを使用し、結果として得られる cdata オブジェクトを保持します。その後、cdata オブジェクトに対して cb:free()
または cb:set()
メソッドを使用します:
-- 明示的にキャストしてコールバックに変換。
local count = 0
local cb = ffi.cast("WNDENUMPROC", function(hwnd, l)
count = count + 1
return true
end)
-- C関数に渡す。
ffi.C.EnumWindows(cb, 0)
-- EnumWindowsは戻り値を返した後、コールバックを必要としないため、解放する。
cb:free()
-- コールバック関数ポインタはもはや有効でなく、そのリソースは再利用される。
-- 生成されたLuaクロージャはガーベジコレクトされる。
コールバックのパフォーマンス
コールバックは遅いです!まず、CからLuaへの変換自体には避けられないコストが伴います。これは、lua_call()
や lua_pcall()
と似ています。引数と結果のマーシャリングもそのコストに加わります。そして最終的に、CコンパイラもLuaJITも、言語の境界を越えてインライン化したり最適化したり、コールバック関数から繰り返し計算を外したりすることができません。
パフォーマンスに敏感な作業にはコールバックを使用しないでください。例えば、ユーザー定義関数を数百万回Cコードから呼び出す数値積分ルーチンを考えてみてください。コールバックのオーバーヘッドはパフォーマンスにとって絶対に悪影響を与えます。
数値積分ルーチン自体をLuaで書く方がはるかに速いです。JITコンパイラはユーザー定義関数をインライン化し、呼び出しコンテキストと共に最適化することができ、非常に競争力のあるパフォーマンスを実現できます。
一般的なガイドラインとして、既存のC APIのために必要な場合のみコールバックを使用します。例えば、GUIアプリケーションでは、ほとんどの時間がユーザー入力を待つため、コールバックのパフォーマンスは関係ありません。
新しい設計では、プッシュスタイルのAPI(C関数が結果ごとにコールバックを繰り返し呼び出すもの)を避け、プルスタイルのAPIを使用してください:新しい結果を得るためにC関数を繰り返し呼び出します。FFIを介してLuaからCへの呼び出しは、その逆よりもはるかに高速です。よく設計されたライブラリはすでにプルスタイルのAPI(読み込み/書き込み、取得/配置)を使用しています。
Cライブラリの名前空間
Cライブラリの名前空間は、共有ライブラリまたはデフォルトのシンボル名前空間に含まれるシンボルにアクセスを許可する特別な種類のオブジェクトです。デフォルトの ffi.C
名前空間は、FFIライブラリがロードされると自動的に作成されます。特定の共有ライブラリ用のCライブラリ名前空間は、ffi.load()
API関数で作成されることがあります。
Cライブラリ名前空間オブジェクトをシンボル名(Luaの文字列)でインデックス付けすると、それが自動的にライブラリにバインドされます。まず、シンボルの型が解決されます — これは ffi.cdef
で宣言されていなければなりません。次に、関連する共有ライブラリまたはデフォルトのシンボル名前空間でシンボル名を検索してシンボルアドレスが解決されます。最後に、シンボル名、シンボルの型、およびそのアドレスの間の結果のバインディングがキャッシュされます。宣言されていないシンボルや存在しないシンボル名はエラーを引き起こします。
異なる種類のシンボルに対する読み取りアクセスでは、以下が発生します:
- 外部関数:関数の型とそのアドレスを持つ cdata オブジェクトが返されます。
- 外部変数:シンボルアドレスが参照解除され、ロードされた値がLuaオブジェクトに変換されて返されます。
- 定数値(static constまたはenum定数):定数がLuaオブジェクトに変換されて返されます。
書き込みアクセスが行われた場合には次のようなことが起こります:
- 外部変数:書き込まれる値は変数のC型に変換され、その後、シンボルアドレスに格納されます。
- 定数変数への書き込みやその他のシンボルタイプへの書き込みは、定数の場所への他の書き込み試みと同様に、エラーを引き起こします。
Cライブラリの名前空間自体はガーベジコレクションされるオブジェクトです。名前空間オブジェクトへの最後の参照がなくなった場合、ガーベジコレクタは最終的に共有ライブラリ参照を解放し、名前空間に関連するすべてのメモリを削除します。これにより、実行中のプロセスのメモリから共有ライブラリが削除される可能性があるため、名前空間オブジェクトが参照されない可能性がある場合には、ライブラリから取得した関数 cdata オブジェクトを使用することは一般的に安全ではありません。
パフォーマンスに関する注意:JITコンパイラは、名前空間オブジェクトの同一性とそれをインデックスするために使用される文字列に特化しています。これにより、関数 cdata オブジェクトは実質的に定数になります。例えば、local strlen = ffi.C.strlen
のようにこれらの関数オブジェクトを明示的にキャッシュすることは有益ではなく、実際には逆効果です。一方、名前空間自体をキャッシュすることは有益です。例えば、local C = ffi.C
です。
手取り足取りはありません!
FFIライブラリは低レベルライブラリとして設計されています。目標は、最小限のオーバーヘッドでCコードやCデータタイプとインターフェイスすることです。これは、Cからできることなら何でもできることを意味します:全てのメモリにアクセスし、メモリ内の何でも書き換える、任意のメモリアドレスでマシンコードを呼び出すなどです。
FFIライブラリは、通常のLuaコードとは異なり、メモリの安全性を提供しません。NULLポインタの参照の解除、範囲外の配列へのアクセス、C関数の誤宣言を喜んで許可します。間違いを犯せば、アプリケーションは、同等のCコードのように、クラッシュするかもしれません。
この振る舞いは避けられません。なぜなら、目標はCコードとの完全な相互運用性を提供することにあるからです。境界チェックのような追加の安全対策を加えることは無意味です。共有ライブラリはシンボル名のみを提供し、型情報を提供しないため、C関数の誤宣言を検出する方法はありません。同様に、返されたポインタの有効なインデックス範囲を推測する方法もありません。
再び言いますが、FFIライブラリは低レベルのライブラリです。これは、注意深く使用する必要があることを意味しますが、その柔軟性とパフォーマンスはしばしばこの懸念を上回ります。CまたはC++開発者であれば、既存の知識を適用するのは容易でしょう。他方で、FFIライブラリ用のコードを書くことは心臓の弱い人には向いておらず、Lua、C、C++の経験が少ない人が最初に取り組むべき課題ではないかもしれません。
上記の帰結として、FFIライブラリは信頼できないLuaコードによる使用には安全ではありません。信頼できないLuaコードをサンドボックス化する場合、このコードにFFIライブラリや任意のcdataオブジェクト(64ビット整数や複素数を除く)へのアクセスを許可することは絶対に避けたいでしょう。適切に設計されたLuaサンドボックスは、標準Luaライブラリ関数の多くに対して安全なラッパーを提供する必要があります。同様に、FFIデータ型に対する高レベルの操作用のラッパーも書かれる必要があります。
現状
FFIライブラリの初期リリースにはいくつかの制限があり、いくつかの機能が欠けています。これらの多くは将来のリリースで修正される予定です。
現在、C言語のサポートは不完全です:
- Cの宣言はまだCプリプロセッサを通していません。
- Cパーサーは、Cヘッダーファイルで一般的に見られるほとんどの定数式を評価することができます。しかし、C式の完全な範囲を扱うことはできず、いくつかの難解な構造に対しては失敗することがあります。
- static const宣言は32ビットまでの整数型でのみ機能します。文字列定数や浮動小数点定数の宣言はサポートされていません。
- コンテナ境界を越えるパックされた構造体ビットフィールドは実装されていません。
- ネイティブベクタータイプはGCCモードまたはvector_size属性で定義できますが、読み込み、保存、初期化以外の操作はまだサポートされていません。
- volatile型修飾子は現在、コンパイルされたコードによって無視されています。
- ffi.cdefはほとんどの再宣言を黙って無視します。注意:C99に準拠しない再宣言は避けてください。実装は最終的に厳密なチェックを行うように変更される予定です。
JITコンパイラはすでにFFI操作の大部分を処理しています。未実装の操作については自動的にインタープリタにフォールバックします(-jvコマンドラインオプションでこれを確認できます)。現在コンパイルされていない次の操作は、特に内部ループで使用された場合、最適でないパフォーマンスを示す可能性があります:
- ベクター操作。
- テーブル初期化子。
- ネストされた構造体/共用体型の初期化。
- VLA/VLSまたは大きなC型(> 128バイトまたは> 16配列要素)の非デフォルト初期化。
- ビットフィールドの初期化。
- 2の冪でない要素サイズのポインタ差。
- 値で渡されたり返されたりする集約を持つC関数への呼び出し。
- 素朴な関数ではないctypeメタメソッドへの呼び出し。
- ctype __newindexテーブルとctype __indexテーブルの非文字列検索。
- cdata型のtostring()。
- ffi.cdef()、ffi.load()、ffi.metatype()への呼び出し。
他の不足している機能:
- 複素数の算術。
- vararg C関数に値で渡される構造体。
- C++の例外相互運用性は、コンパイルされた場合にFFI経由で呼び出されるC関数には拡張されません。