FFIブリッジを作ろう - Sagittarius / DLLを何処に置くか問題

この用途に合うかは分かりませんが、HEADでは(address bv offset)形式でバイトベクタの途中のアドレスを渡せるようにしてみました。

なんと!
というわけで、SagittariusでもDynamic bindingだけでnccc呼び出しを実装することができた。

(library (sagittarius-yuni compat ffi primitives)
         (export yuniffi-nccc-call
                 yuniffi-module-load
                 yuniffi-module-lookup)
         (import (yuni scheme)
                 (sagittarius ffi))
         
(define (yuniffi-nccc-call func
                           in in-offset in-size
                           out out-offset out-size)
  (func (address in  (* 8  in-offset)) in-size 
        (address out (* 8 out-offset)) out-size))

(define (yuniffi-module-load path)
  (open-shared-library path))
         
(define (yuniffi-module-lookup handle str)
  (define ptr (lookup-shared-library handle str))
  (pointer->c-function ptr 'void str '(void* int void* int)))
)

ただ結局専用構文でなくpointer→c-functionを使ってしまった。専用構文だとDLL側のシンボルが自動的にquoteされてしまうので、yuniffi-module-lookupのように、DLLのハンドルとシンボル名を取って手続きを返すような手続きの実装ができない。

Scheme処理系のFFI機能を比較してみて

今回は、nmosh / Gauche / Racket / chibi-scheme / Larceny / Guile / Vicare / Sagittariusで共通のFFI手続きを3種実装した:

  1. (yuniffi-module-lookup ハンドル 文字列) => nccc関数オブジェクト
  2. (yuniffi-module-load 文字列) => ハンドル
  3. (yuniffi-nccc-call 関数オブジェクト 入力スタック 入力スタックオフセット 入力スタックサイズ 出力スタック 出力スタックオフセット 出力スタックサイズ)

これで、yuniFFIが定める呼び出し規約NCCCに準拠するC APIであれば、"DLLをロード→DLLからシンボルをlookup→呼び出し" の一連の処理を処理系独立に記述することができるようになった。
この3手続きの機能は、nmosh / Gauche / chibi-schemeのstatic bindingなFFIを基準に行った。これはyuniffi-module-lookupでクロージャを確保する必要が無いようにするのが目的だったが、動的bindingなScheme処理系、つまり残り全部では結局クロージャを確保することになってしまった。
動的bindingなScheme処理系の機能性はほぼ同一で、複雑なyuniffi-nccc-call手続きを特に問題なく実装することができるが、DLLや.soに指定するパスの解釈がまちまちであり、このポイントもライブラリで吸収しなければならないことがわかった。
RacketとGuileは、拡張子を省略させ、プログラムを環境間で使い回せるように配慮している。これは、WindowsとELF系OSとMacOSでローダブルモジュールの拡張子が異なることによる(Windows = .dll, ELF系 = .so, Mac = .dyld)
Larceny、VicareとSagittariusは、dlopenのセマンティクスを基本的にそのまま採用する。このため、LD_LIBRARY_PATH等が尊重され、拡張子はスクリプト側で正しく与える必要がある。
現状のnmoshでは、ローダブルモジュールの拡張子はOS標準のものを用いず.mplgを使用している。これは、Windowsでは標準では.dllはエクスプローラで見えないので取り回しが悪い(= のでユーザに入れ替えてもらったりといったオペレーションで不便)という酷い理由による。でも実際イベント会場でデバッグログ入りの.mplgに入れ替えてデバッグというのは何度も起きているのでこういう配慮はどうしても必要になる

DLLを何処に置くか問題

まぁDLLの拡張子等は適当に判別すればスクリプト側からでもどうにでもなるので、問題はそもそもDLLをどこに配置するかというポイントになる。
ほとんどの処理系には、そもそもネイティブライブラリを想定したパッケージシステムそのものが存在せず、(nmosh以外の)どの処理系にもアプリケーションを処理系ごと配布するための良い方法が無い。というわけで何らかのプロトコルを発明する必要がある。
代表的なアイデアとしては:

  1. 適当なプレフィックスを付けてLD_LIBRARY_PATHに入れる(yuniffi_libc.so, yuniffi_pthreads.so, ...)
  2. 処理系のライブラリパスに入れる
  3. 独自のライブラリパスに入れる(/usr/lib/yuniffi/yuniffi_libc.so, ...)

が考えられるが、いづれもそれなりのdrawbackが有る。例えば、適当なプレフィックスを付けてpathに入れる方法はWindowsでいわゆるDLL hellを引き起す。処理系のライブラリパスに入れる方法では、そもそもライブラリパスを取得できない処理系が存在するので実装が困難かもしれない。