http://d.hatena.ne.jp/mjt/20170211/p1 prev: 各種処理系のFFIコールバック仕様を見てみる会



yuniではコールバックのシグネチャも一種類に絞ることにしている。yuniのFFI互換層はNCCCなC関数しか呼び出せないが、コールバックとして書けるのもNCCCに絞ることになる。

C言語での NCCC 関数のプロトタイプ

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

SchemeでのNCCC 手続き

( define ( func in in-len out out-len ) ... )

前回( http://d.hatena.ne.jp/mjt/20170211/p1 )書いたように、適当なトランポリンを生成することで任意のC APIをNCCCシグネチャに変換することができる。同様に、Scheme側のコードも適切に生成してやることで普通のScheme処理系と同じような見た目のFFIライブラリを作ることも原理的には可能になる。(通常のシチュエーションではあまり役に立たないのでやらないけど)

移植層のAPI構成 というわけで、移植層のAPI -- つまり、各処理系毎に実装の必要があるものも、以下の3機能に絞ることができる: (Scheme) yuni-nccc-proc-register: SchemeからNCCC手続きを登録する (Scheme) yuni-nccc-proc-release: Schemeから登録したNCCC手続きを抹消する (C) yuni_nccc_proc_call: CからSchemeで書かれたNCCC手続きを呼び出す(処理系によっては存在しない) これらは"普通のCコールバック"がポインタ1つをコンテキストとして受けとれることを前提とする。常識的なC APIは、APIがコールバックを取る場合はユーザデータとしてポインタ1つ分を自由に使えるものとして確保していることが普通で、そのポインタを借用する形でScheme手続きを渡すことになる。

前回の記事で触れたChezやRacketのような動的に関数ポインタを生成できる処理系では、yuni_nccc_proc_call APIは必要無い。別に単純なトランポリンを要求しても良いが、特に理由なくネイティブコードを要求することは避けたいので特別扱いしている。実際にはそれなりの割合のScheme処理系が動的に関数ポインタを生成できる。

つまり、yuni_nccc_proc_callが不要な処理系ではこういうgeneric版をyuniFFIのバインディング側に持たせて使用する: typedef void (*yuni_nccc_proc_t)( uint64_t * in, int in_len, uint64_t * out, int out_len); void yuni_nccc_proc_call_generic( uintptr_t proc, uint64_t * in, int in_len, uint64_t * out, int out_len){ yuni_nccc_proc_t func; func = (yuni_nccc_proc_t)proc; func(in, in_len, out, out_len); } GaucheやChibi-scheme、その他言語組込みの動的FFIが無い処理系ではこのような手続きを処理系毎に用意する必要がある。

処理系のApplyを直接呼ぶ処理系: Chibi-schemeとGauche Chibi-schemeとGaucheには言語組込みのFFIは存在しないため、yuniで独自のFFIを実装している。 Chibi-scheme https://github.com/okuoku/yuni/commit/3ad29063293fa6a6deb9dd1af4c7f7c8a4ad3a02

Gauche https://github.com/okuoku/yuni/commit/1e3cbe6357621518c0bf7e3588e830fb4edc1f54 Chibi-schemeでは、手続きの呼び出しを含めた全てのヒープ操作にctxを渡す必要があるため、コールバックオブジェクトはctxと実際の手続きオブジェクトのconsとしている。 コールバックオブジェクトの生成 static sexp sexp_yuniffi_nccc_proc_register(sexp ctx, sexp self, sexp_sint_t n, sexp proc){ sexp_gc_var2(res, resptr); REQUIRE(ctx, self, proc, sexp_procedurep, SEXP_PROCEDURE); sexp_gc_preserve2(ctx, res, resptr); res = sexp_cons(ctx, ctx, proc); resptr = sexp_make_cpointer(ctx, SEXP_CPOINTER, ( void *)( uintptr_t )res, SEXP_FALSE, 0 ); sexp_preserve_object(ctx, res); sexp_gc_release2(ctx); return resptr; } コールバックオブジェクトの呼び出し static void do_call(sexp ctx, sexp proc, uint64_t * in, int in_len, uint64_t * out, int out_len){ sexp_gc_var1(args); sexp_gc_preserve1(ctx, args); args = sexp_list2(ctx, sexp_make_cpointer(ctx, SEXP_CPOINTER, out, SEXP_FALSE, 0 ), sexp_make_fixnum(out_len)); args = sexp_cons(ctx, sexp_make_fixnum(in_len), args); args = sexp_cons(ctx, sexp_make_cpointer(ctx, SEXP_CPOINTER, in, SEXP_FALSE, 0 ), args); sexp_apply(ctx, proc, args); sexp_gc_release1(ctx); } chibi-schemeはprecise GCを採用しているため、sexp_preserve_objectでコールバックオブジェクトが自動的に解放されないように保護する。手続きオブジェクトを評価するにはsexp_apply()を使う。Chibi-schemeにはGaucheのような引数を直接構築して評価する手続きは無いようだ。

Gaucheではchibi-schemeのような制約は無く、コールバックを実際に呼び出すためには手続きオブジェクト自体が有れば十分となる。 コールバックオブジェクトの生成 ScmObj yuniffi_nccc_proc_register(ScmObj proc){ ScmObj* box; if (!SCM_PROCEDUREP(proc)){ Scm_Error( "proc: must be a procedure" , proc); return SCM_UNDEFINED; } box = GC_MALLOC_UNCOLLECTABLE( sizeof (ScmObj)); *box = proc; return YUNIPTR_BOX(box); } コールバックオブジェクトの呼び出し void callback_bridge( uintptr_t procobjptr, uint64_t * in, int in_len, uint64_t * out, int out_len){ ScmObj* box = (ScmObj*)( void *)procobjptr; Scm_ApplyRec4(*box, YUNIPTR_BOX(in), Scm_MakeInteger(in_len), YUNIPTR_BOX(out), Scm_MakeInteger(out_len)); } GaucheではGenericなCポインタ型は無いためYUNIPTRとして自前のポインタ型を用意している。手続きの呼び出しは呼び出し元のFFI呼び出しコンテキストをそのまま使って良いのでScm_ApplyRec4() APIを使う。

GC_MALLOC_UNCOLLECTABLE()はGaucheのGCであるBoehmGCのAPIで、SCM_MALLOCのようなGaucheが用意しているAPIだとcollectableなオブジェクトになってしまうためここでは直接BoehmGCを呼んでいる。

まだ良く判ってない処理系: RacketとGuile 残りの処理系はScheme側だけで実装できる。 Racket https://github.com/okuoku/yuni/commit/3f1fbec0c8a32d13fa2d5f90de42ccac59e9b8e9

Guile https://github.com/okuoku/yuni/commit/0fbfd4c301911736d850d9dfc22160f9194066ae Racketはfunction-ptr手続きに関数型を渡せばコールバック用のポインタを生成させることができる。 ( define ( yuniffi-nccc-proc-register proc ) ( function-ptr proc nccc-func )) ( define ( yuniffi-nccc-proc-release proc ) #t ) ( define nccc-func ( _fun _gcpointer _int _gcpointer _int -> _void )) yuniffi-nccc-proc-release はどうやって実装するのかわからなかったのでno-opにしている。Racketでは関数型定義の方に生成コールバック手続きを設定できるので、それを使って寿命を管理するのが正しいような気がしている。

Guileはprocedure→pointer手続きを使う。ただ、Scheme側からuncollectableに設定する良い方法が見当たらなかった。 ( define ( yuniffi-nccc-proc-register proc ) ( procedure->pointer void proc ( list ' * int ' * int ))) ( define ( yuniffi-nccc-proc-release proc ) #t ) ( define ( yuniffi-callback-helper ) #f ) とにかく、RacketとGuileではハッシュテーブルか何かにポインタを詰めておいてGCから保護する追加の対策が必要になる。