ライブラリ互換レイヤの制作 - pFFI overview

とりあえず、Racket、Gauche、chibi-schemeとnmoshでR6RS(-light)ライブラリを共有する環境は整ったので、pFFIのランタイムを個々の処理系について実装していくことにする。
pFFIは、pseudo/portable FFIの略で、Scheme処理系間でFFIを使用したバインディングを共有することを目的としている。pFFIは完全なダイナミックバインディングは提供せず、予めUCID形式のC API descriptionを記述しておいたAPIのみ呼び出すことができる。そのかわり、統一されたAPI呼び出しトレース等のイントロスペクション/リフレクション機能を良く提供したい。
(pFFIには一切のGCサポートがない。GC連携はFFIにとっては重要なトピックと言えるが、通常のC APIの呼出はそもそもGCセーフでないことが多いのでpFFIの機能からは落している。また、今のところコールバックのサポートも欠けている。コールバックはそのうちサポートする予定。)

pFFIシステムの立場

pFFIは、いわゆるLL系言語ではあまり見られない"重い"FFIであり、運用上の負担はかなり大きい。pFFIのデザインはperlのXSやSWIGに近い; インターフェースファイルを要求し、そのコンパイラはホスト言語用のライブラリとCグルーコードの両方を生成する。しかし、pFFIはXSやSWIGほどの言語統合は提供しない。
通常、いわゆるLL系言語でC APIを呼び出す場合は以下の2通りが取られる。

  • Dynamic bindingによるFFI

Dynamic bindingは、言語のライブラリとして実装されたFFIインフラを使用する。例えば、luaJITのFFI( http://luajit.org/ext_ffi.html )など。ChezやRacket、nmoshもこちらを通常のFFIとして用意している。
Dynamic bindingのメリットは、新規のライブラリに対応するにあたって、実行環境でのコンパイラが不要になることがある。

  • "拡張モジュール"、static binding

ホスト言語のC言語 APIを使用して関数を提供することができる場合は、そのAPIを外部のCライブラリを呼び出すために使用できる。Gaucheやchibi-schemeはこちらに分類される。
pFFIは、移植性のために両者の"悪いとこ取り"をしている。つまり、Dynamic bindingと同じようにGC/オブジェクトシステムへの統合は困難であり、static bindingのように一度コンパイルしないとライブラリを使用できない。
そこまでして移植性を取る理由は主に2つある:

  1. 多言語拡張。純粋なC API呼び出し機構を処理系を越えて記述することができれば、Rubyや他の言語処理系でもサポートできる可能性がある。
  2. イントロスペクション。多言語サポートであればSWIGを使えば良いが、SWIGにはイントロスペクション考察が無い - pFFIは、引数メタデータを使用して引数の省略やバリデーション、APIトレースのシンボル化(symbolication)を想定している

原理的には、pFFIはdynamic bindingをサポートできる。これは今後検討していく。

pFFIシステムの全体像

pFFIシステムは、以下のように構成される:
UCID API記述。S式。API記述は、pFFIから呼び出すことができるC APIの引数や引数同士の関連、フラグの解釈等を記述したもの。
pFFIランタイムライブラリ(yuni pffi runtime *)。Schemeライブラリ。pFFIランタイムライブラリはScheme処理系上で動作するpFFI処理系実装となる。このライブラリは処理系間で共有される。pFFIインポートライブラリが提供するpFFI関数オブジェクトのイントロスペクションや引数補完等を提供する。
pFFI Cランタイム(yuni pffi compat *)。Gaucheとchibi-schemeで使用されるCコード。dlopen vs. LoadModuleのようなOS間の差を吸収し、stub DLLをロードしてシンボルを参照するためのサポートを提供する。(Racketではdynamic bindingを使用してSchemeコードで記述される。)
pFFI インポートライブラリ(yuni pffi implib CATEGORY ... LIBRARY-NAME)。Schemeライブラリ。UCID記述から生成され、pFFI関数オブジェクトと単純呼び出しのためのScheme手続き、シンボルのためのScheme値をエクスポートする。
callstub。Cソースコード。UCID記述から生成された、API呼び出し規約をwrapし、pFFI呼び出し規約に変換するためのglue code。
stub DLL。callstubとC APIのインポートライブラリ(.libや.so)をリンクしたDLL。
(pFFI VM。pFFI関数の呼び出しに特化したインタプリタGCポーズの影響が大きいリアルタイム処理や、スレッド/fibersのようなGCとの統合に問題のある環境のためにホスト言語から独立した呼び出し環境を提供する。)
UCIDコンパイラ。UCID記述からpFFI インポートライブラリとcallstubを生成するSchemeプログラム。
cmake。UCIDコンパイラはstub DLLをビルドするためのCMake用のプロジェクトを生成する。
例えば、OpenGLを処理系から使いたい場合は:

  1. OpenGL APIのためのUCID記述を作成する。これはKhronosの提供するXMLからほぼストレートに生成できる(が、ある程度のパッチは必要となる)。
  2. UCIDコンパイラによってpFFIインポートライブラリとcallstubを生成する
  3. 生成されたcallstubをCMakeで処理し、stub DLLをビルドする。
  4. pFFIインポートライブラリを使用してSchemeコードを書く

pFFIはポータブルなFFIであるだけでなく、ポータブルなC拡張 APIでもある。つまり、あるSchemeインタプリタをアプリケーションに組込む際に、アプリケーションの内部APIをUCIDで記述し、callstubを静的にリンクすることで、組込むSchemeインタプリタ固有のC APIを使わずにScheme側に関数や定数を提供することが(原理上は)できる。ただし、Cから直接Schemeオブジェクトを生成するような使いかたはできない。Cから入出力するのは数値とbytevectorsに限り、Schemeライブラリ側で残りを世話する必要がある。

拡張モジュールに要求する機能

pFFI Cランタイムを実装するにあたって、Gaucheとchibi-schemeには必要な機能が存在しないので拡張モジュールで補う必要がある。Dynamic bindingでの実装は後回しにする; Racketやnmoshでは拡張モジュールを使用せずに実装できるが、簡単のために処理系側のFFI機能でC ランタイムをロードし、それ経由で関数を操作する。
Gaucheもchibi-schemeも、Win32/POSIX dlopenによるDLL読み込み手続きは存在するが、pFFIの目的には使用できない(dlsymに相当する操作がなく、また、ロードしたDLLに含まれる関数を呼び出すインターフェースが無い)。

  • (pffi-module-load FILENAME) => HANDLE - DLLのロード

pffi-module-loadはDLLをロードする。依存関係も自動でロードする。今のところ、常識的なOSでは依存関係の自動ロードをサポートしているので依存関係は常にロードされる。

  • (pffi-module-unload HANDLE) => boolean - DLLのアンロード

DLLをアンロードすることがある。POSIXはアンロードによってリソースを解放することは要求していないため、アンロードを積極的にサポートする理由が無い。ただし、リロードは検討している - この目的のために、pFFIの関数呼び出しは常に抽象化されたpFFI関数オブジェクトを通して行われる。現状、スレッド間でリロードを同期する良い方法が見当らない。

  • (pffi-module-lookup HANDLE symbol) => value - シンボルのlookup

DLLからシンボルをlookupする。C 呼び出し規約によるシンボルのマングルはcallstub側で面倒を見る(stdcallにしない等)。
pFFIはdynamic bindingでないため、pffi-module-lookupで参照されるシンボルは常にcallstubのシンボルとなる。このシンボルはUCIDコンパイラによって生成され、独自のマングリングも行われる。

  • (pffi-callstub-invoke value value memblk-in in-count memblk-out out-count) - callstubの呼び出し

lookupで得た関数ポインタを呼び出す。本質的にunsafe呼び出しとなる(間違った使用でヒープを破壊できる)。
callstubは常に void CALLSTUB_FUNC(void* value, uint64_t* memblk_in, int in_count, uint64_t* memblk_out, int out_count) のシグネチャとなるため、拡張モジュールがサポートすべき呼び出し規約/シグネチャもこの1種類となる。
通常のcallstubにおいては、memblk-inはbytevectorで、関数の引数となる。memblk-outは関数の出力となる。今のところ、関数の戻り値もmemblk-outを経由して保持する必要がある。nmoshのpffiではin-count等入出力バッファのサイズを表す引数は存在しなかったが、流石にいろいろと事故が起きたので今回は追加することにした。
最適化のために、int戻り値を渡すための専用のシグネチャを用意したほうが良いかもしれない。

  • (pffi-platform-metadata SLOT) => value - プラットフォームデータの取得

C APIにおけるintの長さ、プラットフォームの名前など雑多なデータを取得する。