StubIRのテストを書く

前回決めたStubIRフォーマット( http://d.hatena.ne.jp/mjt/20141127/p2 )のテストを書いた。

当初は、いきなりPOSIXやWin32のStubIRを書いていくつもりだったが、Scheme側のライブラリ仕様を洗練させる必要があるので、nmosh実装とRacket、Gauche実装を同時に進めることにした。その関係上、デバッグの容易なtrivial(= OS依存性の無いpure C)ケースで始める。

StubIRとは

StubIRとは、FFIバインディングを生成するためのS式仕様となる。yuniFFIは、

  1. StubIRからScheme wrapperライブラリとC wrapper DLLのソースコードを出力する "StubIR処理系"
  2. 生成されたWrapper DLLをロードし、C データ型/関数オブジェクトを提供する "yuniFFIランタイム"

の2つを提供する(予定)。これらはyuniのフレームワークで提供されるため、nmoshだけでなく他のScheme処理系でも使用できるように配慮する。
StubIRから生成されたFFIバインディングを実際のアプリケーションで使用するには、一般に、

  • Wrapper ライブラリをWrapするSchemeライブラリ

を更に記述する必要がある。WrapperライブラリはScheme手続きそのものは基本的に提供せず、Wrapライブラリによって適切なSchemeオブジェクトとC APIに渡すオブジェクトの変換が行われることが期待される。
(この分割は、C APIレベルでの呼び出しトレース等inspectionを実現するために採用している。Schemeを単にBetter Cとして使用するならばWrapライブラリを経由せずに直接生成されたライブラリを扱うことも考えられる。)

yuniFFIにおける C API モデル

(あとでちゃんと書く)
yuniFFIでは、C APIの呼び出しをパケットの送受信に抽象化している。つまり、全てのC API関数 F は、入力パケットフォーマットである F.in と 出力 F.out を持つことになる。

;; StubIRの記述
(functions
   (int sum ((intptr input) ;; 最初の引数
             (int count     ;; 2番目の引数
              ;; 2番目の引数の constraint、inputの長さを埋める 
              (count input))
/* Wrap対象となるC API sum() */ 
int sum(int* input, int count){
  int out = 0
  for(int i = 0;i!=count;i++){
    out += input[i];
  }
}

/* StubIR処理系が生成する sum() の入出力パケットフォーマット */
typedef struct sum_in {
   uint64_t input;
   uint64_t count;
};
typedef struct sum_out {
   uint64_t _result;
};

/* StubIR処理系が生成する、sumの type-0 forward stub */
void sum_f0(sum_in* in, sum_out* out){
   int* input;
   int count;
   int result;
   input = in.input;
   count = in.count;
   result = sum(input,result);
   out._result = result
}

/* どのようなシグネチャを持つ関数であっても、forward-0 stubの
 * シグネチャは一定になることに注目。このため、Scheme処理系のFFI機構がサポートすべき
 * シグネチャは1種類に限定することができ、ABI知識を要求しないFFI機構を実現できる。 */

(実際のStubIR処理系はnccc( http://d.hatena.ne.jp/mjt/20141001/p1 )を使用するため、_in や _outの各構造体は生成しない。)
StubIRの記述から、以下が自動生成される:

  1. C言語コード(forward stub) : F.in をうけとって適切なパラメタに展開し、Fを呼び出し、結果を F.out に書き出すC関数
  2. Scheme 関数オブジェクトデータ(Wrapper) : SchemeオブジェクトからF.inを生成、または、F.outからSchemeオブジェクトを生成するためのデータ
  3. (C定数のexport用関数などのユーティリティ)

これにより、FFIランタイムをパケット(blob)とSchemeオブジェクトの相互変換ライブラリと要約することができる。
yuniFFIはforward stubを要求するため、現在のnmoshのようにダイナミックバインディングな処理系とは異なり、ネイティブスタブをビルドする必要が有る。ただし、原理的には、libffiやdyncallのようなABIアダプタに容易に対応できる。

テストの内容

テストの内容はStubIR仕様を良くカバーする。ただし、errno shimとGetLastError shimはOS依存の機能なのでここには含まれていない。また、シンボルのimport/exportもテストには含めていない。
特殊なのはprologueのテストで、実際のバインディングには必要の無いコードを生成させ、独立したヘッダファイル testing.h で検証するという構成になっている。

仕様変更した点

テストを書いているうちに仕様の不備に気付いたのでいくつかの点を修正している。

  • config セクション

StubIR処理系の設定を全てStubIRで完結させるために、configセクションを追加した。現状では、処理系に生成させるファイル名とソース言語を指定するようにしている。
stubを含むDLLのビルドはCMakeやautomakeのような外部のビルドツールで行われるため、ファイル名を明示的に与える必要がある。

  • opaque type の自動生成

従来は、typeに記述されたC typeは、layoutsセクションに記述されない限りScheme側でのインスタンシエートは不可能だった。
通常のシチュエーションでは、pointer-ofとかarray-ofでの派生型以外はScheme側でのインスタンシエートは必須なので、暗黙にlayoutを生成するように変更した。

  • array 指示子の新設

従来の仕様では、配列を含むaggregateを記述する方法が存在しなかった。array指示子を導入し、フィールドをarrayに変更できるようにする。

struct hoge_s {
  int* a0;  /* これは従来から記述可能 */
  int b[4]; /* これを記述する方法が無かった */
};
(aggregate hoge_s
  ((intarray a0) ;; array-ofで生成した型を使用する
   ;; array指示子は、pointer-ofを1段外した型の配列として処理される。
   ;; 妥当な型をフィールド型として指定するのはユーザの責任となる。
   ;; 特に、この表記ではint*の配列にならないことに注意。
   (intptr b array)))

array-ofでは実際に配列が生成されるわけではないのは紛らわしいが、array-ofとpointer-ofを可換にするためにこのような仕様としている。