FFI - Foreign Function Interface のしくみまとめ

FFI とは何か?

ある言語から他の言語で作られたライブラリを呼び出すしくみの事。
ここでは Scheme から C の関数を呼び出す方法をとり上げる。例えば Scheme から libxml の関数を呼ぶなど。

しくみ

上から順に処理が流れる。

ライブラリのロード

呼び出そうとする C 関数を含む共有ライブラリをロードする。ロードに必要な情報は共有ライブラリの名前である。この名前はユーザーが指示する。
dlopen, LoadLibrary などでロードが行われる。この作業は初期化時に一回行うだけでよい。

型情報の提供

呼び出そうとしている関数の引数や戻り値の型を処理系(VM)に提供する。例えば引数は int, char* 、戻り値は void など。
これらの情報はユーザーが手入力したり、FFIの仕組みがヘッダをパースしたり(c-wrapper)することで提供される。
また引数の個数もあわせて必要である。

名前から関数ポインタを得る

呼び出そうとしている関数の名前から、関数のアドレス(ポインタ)を得る。dlsym, GetProcAddress などで行う。

引数を変換しスタックに積む

Scheme の世界から与えられた引数オブジェクトをそれぞれ C の世界で通用する型に変換する。例えば Fixnum -> int や String -> char* など。「型情報の提供」で得られた型情報はここで使用される。
変換された引数はネイティブスタックに積まれる。引数の個数は動的に決まるので呼び出しは func_ptr(a, b, c) の形式では書けない事に注意。

call

関数ポインタを call する。

戻り値を変換

call の戻り値、例えば eax を、型情報に従い Scheme オブジェクトに変換し、Scheme の世界に返す。


callback 引数への対応

C の関数には callback 関数を必要とするものがある。例えば qsort の比較関数。これらの callback 関数には特別の対応が必要である。
callback 関数は

  • ユーザーからは (lambda (x) ...) のように Scheme の世界のクロージャとして渡される
  • Cのライブラリから呼ばれるので C の関数の形をしていなければならない。
  • 上記の呼ばれた関数の中からクロージャを呼び出さなければならない

などの特徴がある。


ここでは C の関数(ネイティブコード)を動的に生成する方法を紹介する。

  • A: 入力として与えられた callback クロージャ (lambda (x) ...)
  • B: A をラップした動的に生成された C の関数

とする。


まず B から呼び出される call_stub をあらかじめ静的に用意しておく。

int call_stub(int id, int argc, void* argv) {
   // id を key に A を取得する
   // argv を Scheme オブジェクトに変換
   // vm->apply(A)
   // 結果を C の戻り値として return
}

call_stub ではグローバルなハッシュテーブルなどに格納された A を id を key として取り出し Scheme の世界で呼び出す。
その結果を C の型に変換して return する。


B はコードを動的に生成して作る。コードを埋め込むのに必要なメモリを確保し、実行可能フラグをつける。
B がやるべき、以下の事をメモリに埋め込む。

  1. A を一意に識別するidを push
  2. 引数の個数と型からスタックの調整を行う
  3. call_stub を call する


以上の内容で初期化された B を実際の callback 引数として渡せばよい。callback 関数が実際に call された場合以下のような動きでうまくいくことが分かる。

  1. B が call される
  2. B から call_stub が call される
  3. call_stub は A をとりだし apply する
  4. その結果が C の型の値として B に戻る
  5. 結果が call 元に戻る

その他のトピック

  • C から Scheme の手続きを呼ぶ
    • リンクして好きに呼べばよい。Scheme 側は C インターフェースを用意する必要あり。
  • Scheme -> C -> Scheme のように C から Scheme 手続き呼びたい
    • Env* ポインタなど世界とのインターフェースを上記の FFI の仕組みの引数に忍ばせる
  • GC との関係(FFIで呼んだライブラリがメモリを参照している場合)
    • 保守的 GC であれば、間違って回収してしまう事はない
    • リファレンスカウント式の場合間違って回収してしまう可能性も。
  • 構造体のマッピング
    • ユーザーが手で変換する
    • 仕組みを用意してあげる
    • padding の問題
  • ユーザーへ提供される API
    • 上記の仕組みは低レベル API と考える事が出来る
    • ユーザーに提供される API は「ユーザーがとにかく楽が出来る」ものを目指すべきである(c-wrapper 最高)

その他

誤りがありましたらご指摘下さい。
色々調べる過程で、Ypsilon の実装を見たのだけど trampoline_t で call ではなくて jmp している理由が分からなかった。Trampoline 調べよう。