FFIブリッジを作ろう - Sagittarius / Guile / Vicare

というわけで、yuniFFIのFFIブリッジ作戦も終盤。Guile / Vicare / Sagittariusの3つはRacket同様libffiベースのFFI機構を持つ。ベースが同じでもAPIはそれぞれなのが興味深い。

Sagittarius

Sagittariusは未遂。Sagittariusはbytevectorの途中をC関数に渡す方法が無いため、nmosh、chibi-schemeGaucheのように追加のネイティブAPIが必要になる。(see comment)
psyntax-moshFFIと同様に、専用の構文を使ってC関数を定義させている。

Guile

(library (guile-yuni compat ffi primitives)
         (export yuniffi-nccc-call
                 yuniffi-module-load
                 yuniffi-module-lookup)
         (import (yuni scheme)
                 (only (guile)
                       dynamic-link
                       dynamic-func)
                 (system foreign))
         
(define (yuniffi-nccc-call func
                           in in-offset in-size
                           out out-offset out-size)
  (let ((inp (bytevector->pointer in (* 8 in-offset)))
        (outp (bytevector->pointer out (* 8 out-offset))))
    (func inp in-size outp out-size)))

(define (yuniffi-module-load path)
  (dynamic-link path))
         
(define (yuniffi-module-lookup handle str)
  (define p (dynamic-func str handle))
  (pointer->procedure void p `(* ,int * ,int)))        
)

guileの場合は、(system foreign)ライブラリにint等のC型とpointer→procedureのようなlibffiによるスタブ生成関数やbytevector→pointerが有り、(guile)の中にdynamic-linkとdynamic-funcの動的ライブラリ系の手続きが有る。これは、Guileは普通のライブラリも.soとして提供できるため(libffiが無くてもshared libraryサポート自体は可能)。
Guileのbytevector→pointer手続きは追加の引数としてオフセットを取る。これは便利。
intはC型オブジェクトだが、ポインタを表わす * はシンボルで使用するあたりが罠か。

Vicare

(library (vicare-yuni compat ffi primitives)
         (export yuniffi-nccc-call
                 yuniffi-module-load
                 yuniffi-module-lookup)
         (import (yuni scheme)
                 (only (vicare)
                       bytevector->memory
                       pointer-add)
                 (vicare ffi))
;; Guile style bytevector->pointer
(define (bytevector->pointer bv offs)
  ;; FIXME: Do we have to use bytevector->guarded-memory ??
  (call-with-values (lambda () (bytevector->memory bv))
                    (lambda (ptr _) (pointer-add ptr offs))))
         
(define (yuniffi-nccc-call func
                           in in-offset in-size
                           out out-offset out-size)
  (let ((inp (bytevector->pointer in (* 8 in-offset)))
        (outp (bytevector->pointer out (* 8 out-offset))))
    (func inp in-size outp out-size)))

(define nccc-callout-maker
  (make-c-callout-maker 'void '(pointer signed-int pointer signed-int)))

(define (yuniffi-module-load path)
  (dlopen path))
         
(define (yuniffi-module-lookup handle str)
  (nccc-callout-maker (dlsym handle str)))
         
)

Vicareは殆どGuileと一緒で、Guile風のbytevector→pointerを手作りしている。bytevector→memoryはポインタと長さの2値を返し、ポインタの加算はpointer-add手続きで行える。これらのヒープ操作手続きは(vicare)ライブラリにある。
関数ポインタのScheme手続きへの変換は、make-c-callout-maker手続きで一旦変換用の手続きを生成してから、その手続きにポインタを渡すことで行える。この2段階が必要な仕組みは不思議に見えるが、libffiの構造からすると自然とも言える。

残った課題

これで、yuniがサポートする予定のScheme処理系で、かつ、FFIが可能なもの(nmosh / Gauche / Racket / chibi-scheme / Larceny / Guile / Vicare / Sagittarius)については一通りFFIブリッジを作成することができた。(Sagittariusはまだだけど、できると仮定)
FFIブリッジが有る処理系では、DLLのロード→シンボルのlookup→nccc形式の関数呼び出し が共通のAPIで行えるが、まだまだ実際のFFI処理を行うには機能が不足している。

Racket等はDLLパスをファイルとして指定させてくれない。このため、ncccのAPIローダ(ncccから本来のC APIへcalling conventionの変換を行うブリッジ)は処理系固有の場所にインストールさせる必要が有る。

  • DLLのアンロードが不可能な処理系が有る(ハンドルが存在しない)

LarcenyはDLLのハンドルの概念が無いため、アンロードを行うことができない。(ゲームとか組込みを除くと)あまり需要のある操作には思えないので、最初のリリースでは省略。

  • 汎用ポインタ型が存在しない処理系が有る

Gaucheには汎用ポインタ型が無いため、bytevectorで代えている。これが良いかどうかは非常に絶妙なポイントで、もしかしたらちゃんとポインタ型をyuniFFIとして定義して利用する必要があるかもしれない。Gauche自体はこれを行うためのインターフェースは用意している。
ただ、処理系固有のCコードは必要最低限にしたいので微妙。

ゴールの再確認


今回FFIブリッジを作った処理系では、ncccなC APIが共通のScheme APIとして呼べるようになった。ただし、これだけでは何の役にも立たない。
C APIであるfopen()を各種Scheme処理系から同一のSchemeコード、"(fopen "hoge.txt" "w+b")"のようにして呼びたいとすると、更に

  1. fopenをnccc呼び出し規約の関数fopen_wrapにラップするDLL
  2. 今回作ったFFIブリッジ、つまり、nccc呼び出し機能を使用してfopen_wrapを呼び出すSchemeライブラリ

の両方を準備する必要がある。人間が手書きでこの2つを用意するのは不毛なので、StubIR( http://d.hatena.ne.jp/mjt/20141127/p2 )と呼ぶS式のフォーマットを決め、Schemeで書いたジェネレータ(= StubIR処理系)でこれらのソースコードを生成することにする。
さらに、StubIR処理系の生成したライブラリから使用される手続き、つまり、ランタイムライブラリを処理系毎に用意する必要もある。
これらを準備できれば、任意のC APIを任意のScheme処理系から同じSchemeコードで呼び出すことができる。これによりゲームのような複雑なC APIに依存したアプリケーションをポータブルに書けるようになり、プログラムの再利用性を高めることができる。