YuniFFIは今のところ10くらいの処理系のScheme→C呼び出しを抽象化している。しかし、コールバックを取るC APIは地味に多いため、コールバックにも真面目に対応しないといけない。。

YuniFFIではScheme→Cの呼び出しAPIをNCCCと呼んでいる特定のCプロトタイプのもの"だけ"実装している。事前にネタバレをしておくと、コールバックも同様となる。

NCCC 関数のプロトタイプ

void func( uint64_t * in, int in_len, uint64_t * out, int out_len);

YuniFFIにおいてSchemeから呼べるのは上記のプロトタイプを持つAPIだけだが、YuniのSDKは任意のC APIをNCCC関数にwrapするC関数(forwardスタブ)を生成するツール付きとしている。このため、forwardスタブを通すことで通常のCライブラリとインターフェースできる。

YuniFFIでのコールバックのサポートとは、即ちScheme手続きをNCCC呼び出し規約を持つC関数ポインタに変換する機構を各種Scheme処理系向けに用意することと言える。

通常のC APIがコールバックを取るときは当然NCCCではないAPI独自のシグネチャを持つ関数ポインタを受ける。このため、何らかの変換機構が必要となるが、コールバック関数にいわゆる"コンテキスト"としてポインタを渡すことができるAPIであれば、NCCC関数を呼び出すための事前に用意したコールバック関数(backwardスタブ)を用意することで、NCCC関数だけをサポートした処理系が任意の処理をコールバックとして提供できる。つまり、コンテキストとなるポインタが指す領域に、NCCCなコールバック関数のアドレスと真のコンテキスト値を置いておき、そちらを呼ぶコードをbackwardスタブとして生成しておけば良い。(コンテキスト引数を持たない qsort のようなAPIは、不正なテクニックを駆使してコンテキスト引数相当の何かを用意する必要がある。あとで書く。)

例えば、コールバックを取るC API foreach_byte() が有ったとして、

void foreach_byte( uint8_t * in, int count, void (* cb)( int c, void * p), void * ctx){ for ( int i= 0 ; i!=count; i++){ cb(in[i], ctx); } }

backwardスタブ foreach_byte_cb_to_nccc() を生成しておけば、

typedef struct nccc_ctx_t { void (*nccc_func)( uint64_t * in, int in_len, uint64_t * out, int out_len); void * ctx; }; void foreach_byte_cb_to_nccc( int c, void * p){ uint64_t param[ 2 ]; uint64_t bogus = 0 ; nccc_ctx_t* ctx = (nccc_ctx_t*)p; param[ 0 ] = c; param[ 1 ] = p; ctx->nccc_func(¶m, 2 , &bogus, 1 ); }

以下のようなNCCC呼び出し規約で実装されたコールバックを、

void my_callback( uint64_t * in, int in_len, uint64_t * out, int out_len){ printf( "called - %d

" ,( int )in[ 0 ]); } void my_callback2( uint64_t * in, int in_len, uint64_t * out, int out_len){ printf( "another one - %d

" ,( int )in[ 0 ]); }

一度nccc_ctx_tにコンテキストをwrapすることで呼び出すことができる。

void testcode( void ){ nccc_ctx_t ctx; ctx.ctx = 0x123456 ; uint8_t in[] = { 0 , 1 , 2 , 3 }; ctx.nccc_func = my_callback; foreach_byte(in, 4 , foreach_byte_cb_to_nccc , &ctx); ctx.nccc_func = my_callback2; foreach_byte(in, 4 , foreach_byte_cb_to_nccc , &ctx); }

↓のような動的なFFIをネイティブサポートした処理系では呼び出しの度にbackwardスタブを経由し、処理系ネイティブの引数ハンドリングの恩恵を受けられない分効率が悪くなる。これは移植性のためということで仕方無い。

Larceny http://www.larcenists.org/Documentation/Documentation0.99/user-manual.chunked/ar01s12.html#id2562963 larcenyはちょっと特殊で、(-> ...)によって関数ポインタを生成できるが生成したトランポリンコードはGCされない。...とりあえずそういうことになっている。回収用のAPIも無い。 The current implementation of arrow types introduces an unnecessary space leak, because none of Larceny's current garbage collectors attempt to reclaim some of the structure allocated (in particular, the so-called trampolines) when functions are marshaled via arrow types. ついでに言うとLarcenyはGCに対する領域の保護もできないため、GC collectableなオブジェクトをSchemeコールバック後にC側で消費すると領域が移動してしまっている可能性がある。というわけで、chibi-schemeやGauche等の動的FFIが無い処理系と同様の考察が必要になる。(以前書いた通り、コールバックが有る場合はバッファのpinが必要 http://d.hatena.ne.jp/mjt/20160913/p1 )