FFIブリッジを作ろう - Sagittarius / Guile / Vicare
- 前々回: http://d.hatena.ne.jp/mjt/20150213/p1
- nmosh / Gauche / Racket
- 前回: http://d.hatena.ne.jp/mjt/20150316/p1
- chibi-scheme / Larceny
というわけで、yuniFFIのFFIブリッジ作戦も終盤。Guile / Vicare / Sagittariusの3つはRacket同様libffiベースのFFI機構を持つ。ベースが同じでもAPIはそれぞれなのが興味深い。
Sagittarius
Sagittariusは未遂。Sagittariusはbytevectorの途中をC関数に渡す方法が無いため、nmosh、chibi-schemeやGaucheのように追加のネイティブAPIが必要になる。(see comment)
psyntax-moshのFFIと同様に、専用の構文を使って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処理を行うには機能が不足している。
- DLLを絶対パスで読ませる方法が無い
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")"のようにして呼びたいとすると、更に
の両方を準備する必要がある。人間が手書きでこの2つを用意するのは不毛なので、StubIR( http://d.hatena.ne.jp/mjt/20141127/p2 )と呼ぶS式のフォーマットを決め、Schemeで書いたジェネレータ(= StubIR処理系)でこれらのソースコードを生成することにする。
さらに、StubIR処理系の生成したライブラリから使用される手続き、つまり、ランタイムライブラリを処理系毎に用意する必要もある。
これらを準備できれば、任意のC APIを任意のScheme処理系から同じSchemeコードで呼び出すことができる。これによりゲームのような複雑なC APIに依存したアプリケーションをポータブルに書けるようになり、プログラムの再利用性を高めることができる。