FFIブリッジを作ろう - nmosh / Gauche / Racketのncccブリッジ
というわけで、nmosh / Gauche / Racket の各Scheme処理系で共通のncccブリッジを実装できた。chibi-scheme(と、Guile2)については後日。
nccc
nccc(Normalized C Calling Convention)は、yuniFFIフレームワークで使用する共通呼び出し規約。yuniFFIで呼び出される関数は全てこの形式でなければならないため、各種C APIを呼び出すためのスタブを生成するツールをyuniFFIには同梱する。
void func(uint64_t* in, int in_size, uint64_t* out, int out_size){ /* 実際の処理 */ }
簡単のため、64bitの可変長パラメタin/outのみを備える。将来のバージョンで、よりメモリ効率の良いインターフェースを模索する可能性は有る。
今回は、このテスト関数を共通のSchemeコード
(import (yuni scheme) (yuni compat ffi primitives)) (define in (make-bytevector 0)) (define out (make-bytevector 0)) ;(define h (yuniffi-module-load "cygyunistub_test_primitives.dll")) ;; Cygwin DLL ;(define h (yuniffi-module-load "yunistub_test_primitives.dll")) ;; Win32 DLL (define h (yuniffi-module-load "yunistub_test_primitives")) ;; Racket FFI (define p (yuniffi-module-lookup h "test0_print_and_fill")) (display "TESTING\n") (display (list 'h: h 'p: p))(newline) (yuniffi-nccc-call p in 0 0 out 0 0) (display "DONE\n")
のコードを使用して呼び出した。このテストコードが(ロードするDLLパスを除いて)nmosh / Racket / Gaucheで同じものであることがミソで、同じプログラムをScheme処理系間で使い回すことができる。
全てのnccc C関数は、Scheme側に、
(nccc_function in ;; <bytevector> in-offset ;; <fixnum> in-len ;; <fixnum> out ;; <bytevector> out-offset ;; <fixnum> out-len ;; <fixnum> )
のような手続きとして公開される。純粋なnccc関数と比べて、bytevectorに対するオフセットの指定が追加されている。これは、Scheme処理系によってはbytevectorの中間ポインタを取るための良い方法が無いため。例えば、inとoutでbytevectorを共通化してアロケーションを減らすといった最適化を可能にする。
nmosh
nmoshには、今のところ拡張(extensions)のための良いフレームワークが無いので、今回の変更はnmoshとyuniの両方のリポジトリに含まれている。
- https://github.com/okuoku/mosh/commit/c632378031e16fd3fa059087a30ba04055c54656
- nmosh側の変更 - nccc呼び出し用CProcedureオブジェクトの追加
- https://github.com/okuoku/yuni/commit/baa77e1f372f3f5fc4d75c5812479f345d153837
nmoshでネイティブ手続きを追加するには、CProcedureオブジェクトを作成してやれば良い。
static Object stub_yuniffi_nccc_call(VM* theVM, int argc, const Object* argv){ /* 中略 */ return Object::Undef; }
CProcedureオブジェクトに変換できるC++関数のシグネチャは上記に固定されている。
作成したCProcedureオブジェクトをScheme VMを通じて公開するにはいくつか方法が有るが、VM命令にマップする方法だとABIを壊すので、nmoshの初期化中に直接グローバル手続きとして追加する。
void register_stubs(VM* theVM){ /* 中略 */ // yuniFFI interface theVM->setValueString(UC("%nmosh-yuniffi-call-nccc"), Object::makeCProcedure(stub_yuniffi_nccc_call)); }
dlopenはnmoshでは(mosh ffi)ライブラリにあるものを流用する。(mosh ffi)のopen-shared-library等は、実はWin64等のネイティブFFIをサポートしていないアーキテクチャ上でも使用できる。
Racket
Racketはlibffiを使用した動的FFIをサポートしているのでそれを使用する。罠としては:
- Racket R6RSのbytevectorはbyte stringにマップされている(マニュアルに有る..?)
- RacketのFFIは、任意のDLLを指定した関数の解決を直接はサポートしていない。移植性のために、".dll"のような拡張子は自動的に補完され、プラットフォーム固有のload pathが仮定される。
全体に、RacketのFFIはPOSIX環境をプライマリとして捉えているように見える。
yuni側の実装は2つのライブラリ(racket-yuni compat ffi primitives)と(yuni-runtime racket-ffi)に分割されている。RacketのFFI構文はR6RS/R7RS構文では記述できないため、nmoshl/R7RSのreaderでも読み取ることができる前者と、Racketのreaderでのみ読み取られる後者に分割した。
(define nccc-func (_fun _gcpointer _int _gcpointer _int -> _void))
Racketの動的FFIでは、_fun構文にC typeを渡すことで関数型を作成する。これを(ffi unsafe)ライブラリのget-ffi-objに渡し、CポインタをScheme手続きに変換することができる。
bytevector(Racket界ではbyte string)をC関数に渡すには、_gcpointerを使う。(ffi unsafe)ライブラリのptr-add手続きを使用することでポインタ演算を行うこともできる。
Gauche
Gaucheは標準では動的FFIを備えていないので拡張を作成する必要がある。拡張のテンプレートはGaucheに同梱されるgauche-packageコマンドで作成できる( http://d.hatena.ne.jp/mjt/20140727/p1 )ので、それで作成したテンプレートを元に作った。
- https://github.com/okuoku/yuni/commit/15ea60c7f0527f2abbd9558889aabb6fed476ea0
- Gauche用の拡張の実装(テンプレートからの差分)
Gaucheには標準でCソースジェネレータであるciseが有り、今回の拡張は全部ciseで書けるので本来はCコードを記述する必要もない。ただ、breakpointを貼ったりしたかったので可能な限りCで書いている。
ScmObj yuniffi_nccc_call(ScmObj func, ScmObj in, ScmObj in_offset, ScmObj in_count, ScmObj out, ScmObj out_offset, ScmObj out_count){ /* 中略 */ return SCM_UNDEFINED; }
作成したC関数をScheme側に公開するには、更にlib.stubファイルを用意し、gauche-packageに渡す必要がある。
(define-cproc yuniffi-nccc-call (func in in-offset in-len out out-offset out-len) yuniffi_nccc_call)
bytevector(Gauche界ではu8vector)は直接C関数に渡すことができる。ポインタ演算を行う簡単な方法は無いので、C関数側でポインタ演算を行っている。
GaucheにはRacketやnmoshのような直接的なポインタオブジェクトは存在しない。代わりにFOREIGN_POINTERクラスを用意しているが、今回はこれを使用せず、bytevectorで代用している。
また、Gaucheにはdlopen等は備わっていないので、これを後で実装する必要がある。代わりに、bootstrapとして一つだけnccc関数へのポインタをScheme側に持ち込む機構を付けた。同じ機構はchibi-schemeでも必要になると思われる。