いろいろ諦めてobject→integerを用意する

前回( http://d.hatena.ne.jp/mjt/20170211/p1 )書いたように、FFIのコールバックでScheme手続きを呼び出すためには、Cオブジェクト、要するにCポインタを通してScheme手続きを受け渡す必要がある。
例えばnmoshにはobject→pointerが有ったり、Chez schemeにはlock-object( http://cisco.github.io/ChezScheme/csug9.4/smgmt.html#./smgmt:s27 )が有ったりして、C←→Scheme界面でのポインタとオブジェクトの変換を合法化する方法が用意されていることが多い。
が、Larcenyのようにそういう抜け道を一切用意していない処理系も存在するため、どうしても何らかの方法でSchemeオブジェクトを固定アドレスで参照する方法を用意しないといけない。。移植層がmatureになるまではobject→pointerが有る処理系でもこちらを使うことにする。
適当に書いてみると:

(define storage (make-vector 8))
(define *index-free-list* (list 0 1 2 3 4 5 6 7))
(define (push-free! idx) (set! *index-free-list*
                           (cons idx *index-free-list*)))
(define (pop-free!)
  (when (null? *index-free-list*)
    (error "Insufficient storage"))
  (let ((r (car *index-free-list*)))
   (set! *index-free-list* (cdr *index-free-list*))
   r))

(define (object->integer obj)
  (let ((idx (pop-free!)))
   (vector-set! storage idx obj)
   idx))

(define (integer->object idx)
  (vector-ref storage idx))

(define (object-integer-release! idx)
  (vector-set! storage idx #f)
  (push-free! idx))

(フリーリストの管理は工夫の余地がある。consだとアロケーションが発生してしまうためフリーリストもvectorにしてキューとして管理するのが良い気がする。)

コールバックの寿命と実行場所

C APIのコールバックにはいくつかのパターンが有る。SchemeFFI APIでこの辺を良く抽象化しているのはRacketで、RacketのC 関数型( https://docs.racket-lang.org/foreign/foreign_procedures.html?q=ffi%2Funsafe#%28def._%28%28lib._ffi%2Funsafe..rkt%29.__cprocedure%29%29 )にはkeepasync-applyの2つのキーワードが用意されている。
keepは、生成したコールバックの寿命、つまり、生成したコールバックをどのタイミングで解放するかの制御に使用される。qsortのようなC APIでは、コールバックはqsort APIを呼び出している間だけ呼ばれるため"親となるC API"の呼び出しの間にだけ保持されていれば良い。このような場合はkeepを偽値とできる。keepを真とすると通常のオブジェクト同様にGCされ(= Scheme側からreachableでなければならない)、他の値を使うことでC オブジェクトとして完全に生存期間を制御することもできる。
async-applyは、callbackを生成したスレッドとは別のスレッドから手続きが呼び出されることを示す。これを直接的にサポートしているScheme処理系は珍しい。
async-applyはYuniFFIの枠でサポートするつもりは無い(そもそもバックエンドとなるSchemeがマルチスレッドセーフでなければサポートできない)が、keepに相当する機能性はサポートする必要がある。
YuniFFIでは処理系のサポートは期待できないため、全てのコールバックは明示的にalloc/freeされる必要がある。このとき、Schemeコールバックは上記のobject→integerで整数(fixnumなコールバック番号)に変換され、C APIに渡される。コールバック番号と実際の手続きの変換はランタイムが面倒を見る。
object→integerを単純にするため一度にobject→integerで割り当てられる数を有限個にしている。実際のプログラムでも、同時にin-flightになるコールバックは有限個であることが十分に期待されるが、コールバックの絶対的な寿命はライブラリによってばらつきが有るのが普通であるため、object→integerも実際の値域をモジュール毎に分割した方が良いかもしれない。
例えばゲームエンジンでは

  • 画面アップデートを行うコールバック
  • キャラクタのアップデート / アニメーションを行うコールバック
  • 通信処理を行うコールバック

等、種々のコールバックがゲームの寿命全体を通じて使用されるため、qsortのようなAPIを使ったプログラムに比べると圧倒的にコールバックの寿命が長くなってしまう。
... コールバックを取るAPIの数だけストレージを用意すれば良いじゃんと思うかもしれないが、そのように取るとqsortのコールバックで更にqsortするようなケースで破綻する。このため、ある種の共通プールはどうしても用意する必要がある。