スタブDLLの作成 - データベースの構成

移植性のある"FFIブリッジ"ライブラリを作成したことで、Scheme処理系間で共通のFFI関数呼び出しフレームワークができた。もっとも、これだけでは役に立たないので実用性のあるFFIフレームワークにするにはまだまだ作らないといけないものがある。
次の目標は様々な世間のライブラリのyuniFFIバインディングをとりあえず作成し、足りない機能が無いかチェックすることにする。特に、ゲームを作るためにはSDL2とOpenGL ES2をバインドしないといけないので当面の目標はそこになる。

  1. [DONE] FFIAPI記述フォーマットであるStubIRを設計する
  2. [DONE] 各種Scheme処理系で動作するFFIブリッジを作成する
  3. [いまここ] スタブDLLのCソースコードを生成する
  4. SDL2のバインディングを作って各種OSでビルドしてみる
  5. DLLローダを書く
  6. 各種Scheme処理系でPONGする

まだまだ先は長い。PONGの後はnmoshの各種機能を移植していき、晴れてnmosh 0.2.8に。。

ランタイムシステムの概要


yuniFFIのランタイムには様々なコンポーネントが存在する。
スタブライブラリは、Scheme側の手続きを表現したライブラリで、実行時にSchemeコードから呼び出される手続きを保持している。簡単のため、今回はStubIRをほぼ単にquoteしたものとする。
スタブDLLは、実行時に読み込まれる、ネイティブコードを含んだ共有ライブラリで、プラットフォーム毎に様々な呼び方がある。DLL(Dynamic Link Library)はWindowsでの呼び方で、Linux等ELF系のOSであれば.soだし、MacOSのようにMACH-Oを使用するOSでは.dyldとなる。ここでは簡単のために呼び方はDLLに統一する。
データベースは、FFIシステムがインポートしてきた手続きや定数を保持するデータ構造で、スタブライブラリによって領域が確保されスタブDLLによって実際のデータが補完される。
StubIRは上記のソースとなるデータで、これはユーザがライブラリの仕様を見て手書きする。SWIGで言うところのインターフェースファイル(.i)に相当する。
バインディングが機能するためには完全なデータベースが必要だが、データベースはスタブライブラリとスタブDLLの両方が無いと完成しない。スタブライブラリ - というかStubIR - は、ライブラリがどのようなAPIや定数を提供するかという情報だけを提供し、その実際の値はスタブDLLが供給する。例えば、APIの関数ポインタは実際にスタブDLLをロードし、動的リンカによるリロケーションが完了するまで確定しない。
スタブDLLはスタブCソースから生成される。スタブソースにはAPI wrapper(yuniFFIは移植性のために単一シグネチャの関数のみ直接呼び出しをサポートしている)と定数データが含まれている。これをビルドすると(Windows)では依存ライブラリ情報が付与される。このため、スタブDLLをロードするとWrap対象のAPIを実装しているDLLもOSによって自動的に(必要に応じて)ロードされることになる。(全てのプラットフォームがこの動作をサポートしているわけではない。)
実際のアプリケーションは、データベースにアクセスしてAPIで使用される構造体のレイアウトを取得したり、APIを起動するための関数オブジェクトを取得したりする。

データベースに格納される情報


ランタイムの本質は、実行時に構築されるデータベースにある。アプリケーションはデータベースに格納された情報を元に、C APIの関数呼び出しパケットを構築して呼び出したり、構造体を解釈したりすることになる。
データベースに格納される情報はfunction(C関数)、constraint(制約)、layout(型と構造体)、export(ライブラリがexportしている定数)が有る。
constraintはちょっと特殊で、S式で表現される。これはスタブDLLから値を供給されない唯一のデータベース要素で、今回は省略。
exportは単純で、シンボル名と値、型のタプルを提供する。
layoutは型の大きさ(size-of)とメンバのリストを提供する。メンバを持つ型は特にaggregateと呼ぶ。メンバはシンボル名、型、size-ofとoffset-ofのタプルとなる。
functionは関数の名前と戻り型、DLLが提供した関数オブジェクトと引数を表す。引数はargumentと呼ぶ。argumentの実質はaggregateと同等だが、aggregateのメンバと異なり、argumentのメンバはsize-ofやoffset-ofを持たない。実装上の都合で、functionは関数ポインタの代わりに処理系依存の関数オブジェクトを格納する。
データベース上の型はCに合わせた固定の型で:

の3種のみ。整数と浮動小数点数はsize-ofで長さ(int32とかfloat)を決めることになる。取り扱えるのはネイティブエンディアンのみ。整数とblobは、値の解釈のためにconstraintのシンボルを追加で持つ。整数型であればenumとしての解釈となり、blobであればaggregateとして解釈される。realではシンボルは使用されない。

DLLからのデータ取り込み

Schemeライブラリとしてのスタブライブラリはconstraintを除いて具体的値を提供しない。というわけで、offset-ofとかsize-ofのデータを埋めるためにはスタブDLLをロードしてそちらからデータを取り込む必要がある。
スタブDLLは実際のAPIスタブや定数データ以外に、値のlookupを行うC関数を内蔵し、処理系はそれを呼び出すことで実際のlookupを行う。(いわゆるdlsymを使用してデータをエクスポートしないのは互換性のため。たぶんOS毎に固有の実装が必要になるので今回は避けている。スタブライブラリもDLLに内包させれば良いじゃんというのはその通りだが、それをやってしまうとeval必須になるので採用できない。)
これを実現するために、スタブライブラリによって構築されるデータベースの"空欄"に名前を付ける必要がある。この名前をラベルと呼ぶ。スタブDLLが提供すべきラベルはスタブライブラリ側に記述される。
ラベルはCシンボルとする。↑の絵では"SDL_DisplayMode_format_offsetof"のように、単純にアンダースコアを使って各要素を連結している。これによって名前の衝突が起こる可能性が有るが、それは実際に起きてから考えることにする。

次の一手

次の一手はスタブCソースの生成になるが、これには独立した2つのステップが有る:

  1. Constant tableをStubIRから生成する
  2. NCCC call stubをStubIRから生成する

まずはConstant tableの生成から始める。既にCマクロは用意しているので( https://github.com/okuoku/yuni/blob/ac93456515136d2ba418e99da2b9f17f0d2d5efe/tests/yunistub/trivial/hand.inc.c#L63 )、ラベルを生成した上でStubIRの内容に従って並べるだけの簡単なお仕事。