スタブDLLの作成 - 定数テーブル

というわけで、DLLが提供すべき定数テーブルのコードをStubIRから生成する。

定数テーブルとは

定数テーブルとは、"ABIを構成する定数"の表を示す。要するにFFIバインディングの関数定義以外の部分と言える。
定数テーブルには以下のような情報が含まれる:

  • APIが提供している定数の実際の値。例えば、Cygwinのerrno値の一つであるENOENTは2となっている。(このエラー番号は太古から存在するからか、大抵のOSでは2になっている。)
  • APIが提供している型の実際のサイズ。例えば、Cygwinstruct direntの大きさは280バイト有る。
  • APIが提供している構造体のフィールドのoffsetとサイズ。

StubIRは、APIが提供している型や定数、構造体(aggregate)の各フィールドを全て記述することができるように設計されている。このため、StubIRを"定数テーブルを出力するCコード"に変換することで、効率的に定数テーブルを得ることができる。

定数テーブルをDLLから提供する理由

定数なんて事前にわかるんだからDLLに置かなくても良いような気もする。実際には、いくつかの理由で定数であってもDLLに配置する必要がある:

  • ABI安定でないAPIセットが存在する

APIの中には、あまり"ABI(Application Binary Interface)安定性"を意識せずにデザインされているケースが有る。ABI安定はNMoshの造語で、互換性のある過去のバージョンのヘッダでビルドしたバイナリが、そのまま最新のライブラリに動的リンクしても動作することを指す。
ABI安定でないものでメジャーなものにはMSのC++ランタイムがある。MSのC++ランタイムは標準にそれなりに準拠しているが、VisualStudio2010でビルドしたバイナリはVisualStudio2010のランタイムと共に使用する必要がある。

  • API仕様がABIを規定しないケースが有る

POSIXのようなAPI仕様は、ENOENTのようなerrnoシンボルは提供するものの、実際の値は規定しない。このため、Schemeコード側にこれらの定数を置くことにしてしまうとFFIバインディングを個々のOSの個々のlibc毎に作成する必要がある。
... もちろん定数テーブルから提供されるデータは本質的には定数であるため、将来のバージョンではキャッシュを提供することになる。

フラット化データベース

DLLからのデータ入力を簡単にするために、フラット化したデータベースフォーマットを用意する。

  フラット化データベース ::= ("データベース名" (<エントリ> ...))
  エントリ ::= (constant        "ラベル"   sizeof <ifdef> ...)            |
               (layout          "ラベル"  C型種別 sizeof <ifdef> ...)       |
               (aggregate-entry "ラベル"  "親のラベル" "C参照名" sizeof offset <ifdef> ...)
        :: = signed | unsigned | real | blob | pointer
  C型種別 :: = value | c-struct | c-enum | c-union
    ifdef :: = (ifdef "Cマクロシンボル")

(この定義にはfunctionやconstraintが含まれていない - これらの扱いは次回以降)
それぞれ微妙に違うエントリを持っている。ラベルの付与規則は前回のエントリからちょっと変えていて、Cシンボルセーフではなく、エントリをスラッシュで区切り、一つのエントリでsizeofとoffsetの両方を提供するように変えた。エントリの総量を減らすため。スラッシュ区切りのラベルは依然Schemeシンボルとしては有効なデータだが、必要の無い限りシンボルをinternしないようにしている。
layoutだけがC型種別を持っている。C型種別は、その型を持つ変数をインスタンシエートするのに必要なプレフィックスを指す。valueであればプレフィックスなし。c-structであれば、"struct hoge"のようにstructを前置する等。
ifdefは、文字通りCコードを指定されたマクロのifdefで囲む。

例えば、

  • (layout "hoge_s" blob c-struct 32) → 型"hoge_s"はScheme側にはBytevectorとして公開され、サイズは32バイト、スタブCソース上でインスタンシエートする場合はstruct hoge_sのように書く必要が有る。
  • (constant "E_B" unsigned 1 4) → 定数"E_B"は値1、サイズ4(32bits)
  • (aggregate-entry "hoge_s/m_d/mm_b" signed "hoge_s" "m_d.mm_b" 4 28) → aggregateのメンバ"hoge_s/m_d/mm_d"は、layout "hoge_s"をオフセット基準とし、スタブCソース上ではhoge_sのメンバ"m_d.mm_b"として参照され、オフセット28バイト、サイズは4バイトの位置にある

... 図のフラット化データベースには既にsizeofやoffsetが埋まっているが、これらのデータはまだStubIR処理系はできてないので手動で計算した。つまり、

#define ASIZEOF(type,member) \
    sizeof(((type *)0) -> member)

int
main(int ac, char** av){
    printf("hoge_e: sizeof = %d\n",sizeof(enum hoge_e));
    printf("E_A: value = %d sizeof=%d\n",E_A,sizeof(E_A));
    printf("E_B: value = %d sizeof=%d\n",E_B,sizeof(E_A));
    printf("hoge_s: sizeof = %d\n",sizeof(struct hoge_s));
    printf("hoge_s/m_a: sizeof = %d, offset = %d\n",ASIZEOF(struct hoge_s,m_a),offsetof(struct hoge_s,m_a));
    printf("hoge_s/m_b: sizeof = %d, offset = %d\n",ASIZEOF(struct hoge_s,m_b),offsetof(struct hoge_s,m_b));
    printf("hoge_s/m_c: sizeof = %d, offset = %d\n",ASIZEOF(struct hoge_s,m_c),offsetof(struct hoge_s,m_c));
    printf("hoge_s/m_d: sizeof = %d, offset = %d\n",ASIZEOF(struct hoge_s,m_d),offsetof(struct hoge_s,m_d));
    printf("hoge_s/m_d/mm_a: sizeof = %d, offset = %d\n",ASIZEOF(struct hoge_s,m_d.mm_a),offsetof(struct hoge_s,m_d.mm_a));
    printf("hoge_s/m_d/mm_b: sizeof = %d, offset = %d\n",ASIZEOF(struct hoge_s,m_d.mm_b),offsetof(struct hoge_s,m_d.mm_b));
    printf("hoge_t: sizeof = %d\n",sizeof(hoge_t));
    printf("D: value = %d sizeof = %d\n",D,sizeof(D));
    return 0;
}

のようなCコードを人力で書いて、Cygwinで出力している。

hoge_e: sizeof = 4
E_A: value = 0 sizeof=4
E_B: value = 1 sizeof=4
hoge_s: sizeof = 32
hoge_s/m_a: sizeof = 4, offset = 0
hoge_s/m_b: sizeof = 1, offset = 4
hoge_s/m_c: sizeof = 16, offset = 8
hoge_s/m_d: sizeof = 8, offset = 24
hoge_s/m_d/mm_a: sizeof = 1, offset = 24
hoge_s/m_d/mm_b: sizeof = 4, offset = 28
hoge_t: sizeof = 32
D: value = 10 sizeof = 4

本来、StubIRからフラット化データベースを生成するだけでは、これらの値は#fとなっていて、実際にDLLからデータをインポートすることで具体的な値が埋まることになる。今回のスタブCソース生成の目的は、StubIRからこのようなCコードを生成することと同じと言える。
実際のスタブCソースはテキストを出力するのではなく、ラベルに合わせた値(value/sizeof/offset)を出力する関数で構成される。
重要なポイントは、

  • 上記のCコードはABI独立であり、一度生成すればコンパイル/実行するだけでどのC言語環境でもABIの抽出に使用できる
  • APIとして記述されている構造体のフィールドが一部だけであっても(= プライベートなメンバが有ったとしても)、常に適切なsizeof等の値を出力できる

点。

実装

... 大急ぎで書いているので特にコメント無し。。たぶんそのうちゼロから書き直すので、今回は動けば良しということで。
StubIRで設定した型情報をC型に対応づけるのが意外とtrivialでなかった。このため、resolve-type手続き( https://github.com/okuoku/yuni/commit/5eeaa7c6a1ffa8398e09938df79397122f8d9f0e#diff-b562716e9862687a82d0045cfdf5068dR52 )は全く実装していない。また、生成したフラット化データベースをCソースに変換する部分も無い。
実装ではR6RSのfilter手続きを使用している。後で置き換える。

次回

次回はC型の決定アルゴリズム。StubIRでは様々な目的のために"型"を定義させているが、これをC言語上の型、つまり、signed / unsigned / real / blob / pointerのいづれかに対応づける必要がある。