C API ブリッジのデザイン
で、作ったジェネレータ( http://d.hatena.ne.jp/mjt/20141019/p1 )を使ってnccc( http://d.hatena.ne.jp/mjt/20141001/p1 )へのブリッジソースジェネレータを作る。
yuniFFIにおけるC API呼び出しの考え方
yuniFFIでは、C API呼び出しをパケットの送受信として捉えている。つまり、C API記述は本質的にS式とパケットの相互変換ルールと言える。
例えば、通常のAPI呼び出しは1パケットの送信(引数)と1パケットの受信(戻り値)と言える。callbackはパケットの受信(callbackの引数)に相当する。構造体を取るAPIについては、まさに、構造体自体がパケットに相当する。
パケットとS式を相互変換する良い方法を提供することで、それは良いFFIとしても使用できると考えている。
... が、実際にはC API呼び出しをこのレベルにまで抽象化することが最初に必要で、その抽象化がncccと言える。
最初の実装(nmosh 0.2.8)では、明示的な長さのbytevector arrayとC文字列、bytevectorと数値の相互変換だけをサポートする。例えば、Schemeシンボルを自動的にフラグ数値に変換してくれたりはしない。
デザイン
C APIの記述に必要な情報はかなり複雑なことになっている。
全ての情報のルートとなるのはC libraryオブジェクトで、Scheme側ではyunilibと呼ばれる。yunilibは複数のTranslation unitsから成り、translation unitひとつは .c ソースコードひとつに相当する。ひとつのyunilibが、ひとつの .so とSchemeライブラリを生成することになる。
Translation unitは、ExportとTypeで構成される。Exportは、最終的にyunilibでエクスポートされるシンボルに相当し、TypeがFFI記述の本体に相当する。
Typeは以下の5種類が有る
- Scalar。intとかdoubleのような数値型。
- Pointer。scalarの特殊型。
- Array。scalarの特殊型。pointerとarrayは、後述のconstraintを使用して特別な意味を持たせることができる。
- Aggregate。structやunion。Aggregateは複数Entryを持つことができ、Entry間にはconstraintを適用することができる。
- Function。Aggregateの特殊型だがスーパーセットでない(たとえばsizeofが無い)。
AggregateはAggregateEntryにおけるsizeof/offsetofに加えて、全体のsizeofも持つ。これは、yuniFFIでは構造体の不完全な記述を許しているため。POSIXではstatのような構造体が拡張されているのはよくあることなので、不完全な記述(= 全ての構造体メンバを記述しない)でも動作するように配慮する必要がある。
Function型自体はn in 1 outまたはn in 0 outとなっている。FunctionArgumentEntryの方で、引数のin / out / inoutを指定することになる。
aggregateやFunctionのentry間に適用できるconstraintは、nmosh 0.2.8では3種類を用意する予定。
- Value。定数。通常はaggregateで使用する。
- Count。ターゲットの配列の長さやバイト数を埋める。
- CString。バッファがC文字列を指すことを指定する。
ValueConstraintはReserved引数には使用しない。引数がReservedでなくなったときにこのconstraintを外すと、(FFIでエクスポートされる)APIが変化してしまう。
ToDo
- LengthConstraint。pipe()のように配列の長さが一定であることが決っている場合はConstraintにすれば良い。
- 可変長引数関数をどうすんのか問題。
- Translation unitがエクスポートする定数間のConstraintをどう記述するのか問題。
- shim( http://d.hatena.ne.jp/mjt/20141001/p1 )の統合
ToDoを消化した場合の理想のpipe(2)バインディング
pipeシステムコール( http://linuxjm.sourceforge.jp/html/LDP_man-pages/man2/pipe.2.html )は、本質的には0 in 3 outの関数となっている。
() => [pipe] => (errno, fd0, fd1)
この本質のまま、Scheme手続きpipeを作ることができれば、pipe手続きは3つの値を返す手続きとなることが期待される。
(receive (errno fd0 fd1) (pipe) (display (list 'err: errno 'fds: (list fd0 fd1)))
API記述は以下のようになり、
(yunilib (posix unistd) (prologue "#define _XOPEN_SOURCE 700" "#include <unistd.h>") ;; 中略 (array array-of-int int) (function pipe int ((fds array-of-int ;; 引数はout方向に使用(入力スタックを消費しない) out ;; Constraints ; Arrayタイプの長さを2に指定 (length 2) ; Arrayをexpandして出力スタックを2つ分消費 expand)) shim: errno) )
生成されるtranslation unitは、
#define _XOPEN_SOURCE 700 #include <unistd.h> void export_pipe(uint64_t* in, uint64_t* out){ int fds[2]; int out0 = 0; int r; r = pipe(&fds[0]); if(r){ out0 = errno; } out[0] = out0; out[1] = fds[1]; out[2] = fds[2]; }
このような変換を行うことで、直感的なScheme手続きを出力することができ、Scheme側では値を受信するためのbytevectorの割り当てを節約することができる。