ネイティブコードインターフェースの抽象化

初めて?yunibaseがビルドの失敗を検出した。というわけで報告して直した:

やっぱり毎日くらいのペースで定期的にビルドした方が良いのかな。。方々のScheme処理系に非常にどうでも良いdiffを入れる奴になりつつあるのでもうちょっと実の有るコントリビューションをしたいところ。。
nmoshの営業政策として、

  1. 各種Scheme処理系で動作する共通ライブラリyuniを整備
  2. yuniのプライマリな処理系はnmosh
  3. 処理系の差異化要素はnmosh専用 - エラー表記が微妙に親切なフロントエンドとか、コアダンプとか

というのを考えていて、yuniにFFIが統合されているのは営業上非常に重要な項目になっている。SchemeのライブラリセットにはSLIBのようなメジャーどころは有るにしても移植性のあるFFIバインディングなんてクソ面倒なものは誰もやらないだろうという計算。でもlibarchiveとcurlバインディングだけでも出来ることはかなり広がるし、SDLの良いバインディングがあればゲームも書ける。
で、既存のR6RS/R7RSだいたいで動くFFI抽象化層を一度は書いてみたけど、もうちょっと真面目に抽象化しないと不味いということが解ってきた。

Bytevectorの扱いと内部表現

JavaScriptのTypedArrayに相当するデータ領域はR6RS/R7RSではbytevectorとして表現される。この実装方針は大きく分けて2つ:

  • Separate: bytevectorを表わすオブジェクトと、その内容を保持するメモリ領域を別々に確保する
  • Combined: メモリ領域にbytevectorを表わすヘッダを付ける

理想的には処理系は両方を備えるべきで、それぞれにpros/consがある。

  • Separate方式(左)はメモリを2箇所に確保する必要がある。Combinedは1箇所で良いので、非常に小さなbytevectorを多数持つようなケースで効率的と言える。
  • Combined方式(右)は外部から取得したメモリ領域をbytevectorにしたり、同一のメモリ領域を指す異なるbytevectorを持てない。Separate方式は、例えばmmap()で確保した領域等OSから取得した領域を直接bytevectorとしてSchemeプログラムに公開することができる。

yuniFFIを設計した当初はSeparate方式を想定していたので、Combined方式を採るVicareとかLarcenyのような処理系が良くサポートできない状況になっている。Combined方式の処理系では、Scheme側からBytevectorでアクセスできる全ての領域をScheme側で確保しなければならないため、API側でメモリを確保してくる場合は出力を参照する際に必ずコピーが発生してしまう

bufferオブジェクトとpointerオブジェクト

選択肢として考えられるのは、bufferオブジェクトを導入しそもそもbytevectorを直接扱わないようにAPIを変更すること。つまり、Combined方式であっても、bytevectorでない専用の型(buffer)を用意してSeparate方式をエミュレートするということになる。
これをやってしまうと、当然Scheme側のbytevector関連手続きとの相互運用性が無くなってしまう。この点については、yuniのライブラリとして汎用のフェッチや構造体のfillを出すことで埋め合せができるのではないかと考えている。
Combined方式を取る処理系はだいたいbytevector→pointerpointer→bytevectorができない。要するにbytevectorのデータが含まれるアドレスは常に変化する可能性があると想定してコードを書くしかない。
... 結局のところ移植性を確保したまま、汎用的なC呼び出しを行うためには、GCによる管理を諦めてmalloc/freeによる管理を強制するしか無いんだろうか。
もちろん、いくつかのクラスの処理(e.g. 無関係なスレッドにデータを渡す、C APIの呼び出しを超えて寿命を確保する必要のあるデータ 等)はmallocしたバッファにデータを移してから呼ぶしかない。このためbufferオブジェクトとしてメモリ領域を抽象化することに一定の意味は有るが、パラメタを構造体で渡すAPIとかOSに対するread/writeのように、呼び出し中だけ寿命が有れば十分APIもかなりの数が有る。

FFI関数呼び出し仮想機械

というわけで、FFI関数呼び出しのためのバイトコードインタプリタを導入するのはどうか。

ここでは、2つのバッファ内に確保された変数3つを取るFFI関数 func の呼び出しを考える。

  • (上) Separate方式の処理系では、変数それぞれのポインタを取ることができるので、funcに直接ポインタを渡せば良い。
  • (下) Combined方式の処理系では、bytevectorの先頭アドレスしか渡せないので、bytevector0とbytevector1、パラメタへのオフセットd0〜d2、更にバイトコード命令を詰めたbytevector2をパラメタとして渡し、インタプリタを起動する。

このようにすることで、Scheme上ではbytevectorの具体的なアドレスを知ることなく(オフセットさえ判っていれば)関数のパラメタとして使用できる。
また、このような方式を取ることで、Scheme ←→ ネイティブ境界を行き来する回数を減らせる可能性がある。