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の両方のリポジトリに含まれている。

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のFFIPOSIX環境をプライマリとして捉えているように見える。
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 )ので、それで作成したテンプレートを元に作った。

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でも必要になると思われる。