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にすれば良い。
    • ExpandConstraint。pipe()のようなshort vectorを入出力するAPIでは、引数を増やす方向の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の割り当てを節約することができる。