stubIR変換処理の作成
(諸般の事情でUCID/pFFIはyuniFFIに改名される。GenericなAPI記述はC++サポートのために再検討を入れ、yuniFFIはSchemeに一旦特化する。)
stubIRはCコードに変換され、Cコンパイラで処理されて呼び出し用のstub(DLL)となる。stubはScheme処理系固有の方法でロードされ共通のAPIを提供する。
プリミティブ
stubの提供するプリミティブは:
- シンボル→数値変換 (yunistub-ref [yunistub-lib] [SYM]) => integer / #f
- シンボル→ポインタ変換 (yunistub-ref/pointer [yunistub-lib] [SYM]) => pointer / #f
のみで、手続きの呼び出しや構造体のアクセスは、これらを使用して構築される。(C関数ポインタのscheme側からの呼び出し等は処理系毎に実装されるランタイムライブラリ側に有る)
yuniFFIは独立したポインタ型を持つ。これは、ポインタ値は通常fixnumよりも大きいためbignumとして処理されてしまう可能性が有ることと、ポインタを独立型とすることでGC統合の可能性を作っている。つまり、integer→pointerのような手続きを使うとGC安全でないコードを書く可能性が高いため注意を要する。
この方式はランタイムのシンボル生成に依存しているため非常に効率が悪い。処理系固有の最適化を盛り込む必要が有る。
stubはABIによって変化する可能性のあるデータのみをエクスポートする。API記述は同時にSchemeライブラリも出力し、あるシンボルの型(C手続きなのか値型なのか等)はそちらに記述される。この分割によって、スクリプトをABIの変化に対してrobustにすることができる。その代わり、stubは同時に生成されたSchemeライブラリと組み合わせて使用する必要がある。
同様の理由で、stubがエクスポートしているシンボルを列挙する方法は存在しない。
プリミティブをこのレベルに限定するのは、クロスABI抽出( http://d.hatena.ne.jp/mjt/20140621/p1 )との互換のため。関数ポインタ以外は静的に抽出するチャンスが有る。
C constructsのプリミティブへの変換ルール(定数)
TODO: 構造体内のビットフィールドをエクスポートする方法が無い
単一のシンボルは単一の値しかエクスポートしないため、シンボルをmangleすることで必要なメタデータを収集できるようにする必要がある。メタデータの付与は/(スラッシュ)で区切ったシンボルで行われる。
- 単体型
通常の型はsizeofをエクスポートする。
以降の例に出てくるexportは実際にはいくつかのアーキテクチャでは不可能なことに注意する。実際のexportは関数呼び出しで行われる(後述)。
typedef int hoge_t; yuniffi_export __exports__[] = {{ "hoge_t/sizeof", sizeof(hoge_t) }};
- 構造体
構造体は全体のsizeofと個々のフィールドのoffsetofを出力する。個々のフィールドの型はSchemeライブラリの方に記録される。
C APIで使用される構造体は、型としてtypedefされるものと、構造体として定義されるものの2通りが有る。stubIRはこの両方が表現できなければならない(型として定義された構造体を構造体に戻すことはできないので、生成されるCコードでは、struct hoge_sとhoge_sの両方の表記が必要となる。)
offsetofはCではないが通常stddef.hで提供される。必要ならばマクロで定義でき、clangのようなコンパイラはintrinsicを持つ。
struct hoge_s{ int a; }; yuniffi_export __exports__[] = { { "hoge_s/sizeof", sizeof(struct hoge_s) }, { "hoge_s-a/offsetof", offsetof(struct hoge_s, a) } };
- enum / 定数マクロ
定数はシンボル名に直接値を記録するものと、ポインタを記録するものに分かれる。どちらが記録されているかはSchemeライブラリの方に記録される。
enumは実際のC言語と異なり構造体風にnamespaceされる。
#define HOGE 10 enum hoge_e { A = 10, B }; yuniffi_export __exports__[] = { { "HOGE", HOGE }, { "hoge_e-A", A }, { "hoge_e-B", B } };
関数型のエクスポート
TODO: 可変長引数のサポート
関数型のエクスポートは、実際のコードを.textに必要とするという点で他のエクスポートと異なる。
yuniFFIにおけるC関数は、Generic C Calling Convention(gccc: 紛らわしいので変更するかもしれない)での呼び出しに統一されている。つまり、Scheme側でuint64_t配列に構築されたCスタックを、stub内のコードで通常の呼び出し規約に変換する。エクスポートされるのは、この変換または逆変換を行う関数の関数ポインタとなり、API自体の関数ポインタではない。
stubとして生成される関数は、Scheme→C呼び出しを行うforward stubと、C→Scheme呼び出しを行うbackward stubの2種類に大別できる。Forward stubは通常のSchemeからC APIを呼び出す際に使用される。Backword stubはcallback等、C APIからScheme側を呼び出す際に使用する。
Forward stubには、呼び出し先の関数ポインタを取るtype 1と、取らない(= 呼び出し先が真に固定である)type 0の2種類が生成される。type 1 forward stubは、OpenGLのような"拡張APIについては、個々に関数ポインタを取得して呼び出す"スタイルを取るAPIに対応するために使用される。type 0 forward stubは、通常のAPI呼び出しだけではなく、インスタンシエートされたマクロの呼び出し等内部的な用途にも使用される。
Backward stubは、グローバル変数として定義された呼び出し先ポインタ(linktarget)をコンテキストデータ(linkcontext)を伴なって呼び出すtype 0のみが存在する。多くのcallbackは、コンテキストとしてvoid*を取るように設計されるので、それを活用したtype 1を将来追加する。type 0は、グローバル変数として取られる領域がもったいないという問題が有る。
Forward/Backwardスタブは、両者ともvoid関数として定義され、戻値は供給されたポインタに書かれる。これは、stubが多値を返却できるように配慮している(C関数は多値を戻すことは無いが、計時機能を持ったstubの生成等の目的で使用される)。
static int // Sample API to be exported hoge_capi(int param0, void* param1, int param2){ return *(int*)param1 + param0 + param2; } // Generated C API caller type typedef int (*hoge_capi_QQ_type)(int, void*, int); // Generated global link table void* __global_linktable_hoge_capi_QQ_backward1_func; uint64_t __global_linktable_hoge_capi_QQ_backward1_context; // Forward type0 stub static void hoge_capi_QQ_forward0(uint64_t* in, uint64_t* out){ uint64_t r; r = hoge_capi((int)in[0], (void*)(uintptr_t)in[1], (int)in[2]); if(out){ out[0] = r; } } // Forward type1 stub static void hoge_capi_QQ_forward1(uint64_t* in, uint64_t* out){ uint64_t r; r = ((hoge_capi_QQ_type)(uintptr_t)in[0])((int)in[1], (void*)(uintptr_t)in[2], (int)in[3]); if(out){ out[0] = r; } } // Backward type1 stub static int hoge_capi_QQ_backward1(int param0, void* param1, int param2){ uint64_t in[4]; uint64_t out; in[0] = (uint64_t)__global_linktable_hoge_capi_QQ_backward1_context; in[1] = (uint64_t)param0; in[2] = (uintptr_t)param1; in[3] = (uint64_t)param2; ((yuniffi_callstub)__global_linktable_hoge_capi_QQ_backward1_func)(in, &out); return out; } yuniffi_export __exports__[] = { { "hoge_capi/forward0", hoge_capi_QQ_forward0 }, { "hoge_capi/forward1", hoge_capi_QQ_forward1 }, { "hoge_capi/backward1", hoge_capi_QQ_backward1 }, { "hoge_capi/backward1/func", __global_linktable_hoge_capi_QQ_backward1_func }, { "hoge_capi/backward1/context", __global_linktable_hoge_capi_QQ_backward1_context } };
この方式は、qsortのようなcontextを取らないcallbackを使用するAPIをスレッド安全に使用することができない。このようなcallbackを取る関数は必ずcallbackを呼びだすトランポリンコードを生成する必要がある。
実際のexport処理
実際のexportは関数の呼び出しによって行われる。これは、いくつかの(殆どの?)アーキテクチャについて、関数はリロケーションが行われ、関数に対するリロケーションを.dataに適用することができないことによる。
これを回避するためには、シンボルテーブルを取得するための関数を用意し、これを呼ぶことになる。
ABIバージョンのチェック等もここで行う。
void do_export(uint64_t* in, uint64_t* out){ typedef struct { const char* name; uint64_t value; }yuniffi_export; yuniffi_export exports[] = { { "hoge_t/sizeof", sizeof(hoge_t) }, { "hoge_s/sizeof", sizeof(struct hoge_s) }, { "hoge_s-a/offsetof", offsetof(struct hoge_s, a) }, { "HOGE", HOGE }, { "hoge_e-A", A }, { "hoge_e-B", B }, { "hoge_capi/forward0", (uintptr_t)&hoge_capi_QQ_forward0 }, { "hoge_capi/forward1", (uintptr_t)&hoge_capi_QQ_forward1 }, { "hoge_capi/backward1", (uintptr_t)&hoge_capi_QQ_backward1 }, { "hoge_capi/backward1/func", (uintptr_t)&__global_linktable_hoge_capi_QQ_backward1_func }, { "hoge_capi/backward1/context", (uintptr_t)&__global_linktable_hoge_capi_QQ_backward1_context } }; int i; const uint64_t total = sizeof(exports)/sizeof(yuniffi_export); const uint64_t in_abi = in[0]; const uint64_t in_count = in[1]; if(YUNIFFI_ABI == in_abi){ out[0] = total; for(i=0; i!=in_count;i++){ if(total==i) break; out[i*2+1] = (uintptr_t)exports[0].name; out[i*2+2] = exports[1].value; } } }