SMLSharpでFFIバインディングを書く時の知見

κeenです。最近頻繁にSML#を使ってます。SML#のメイン機能の1つであるC連携ですが、ちょっと複雑なことをやろうとするとテクニックが必要になるので共有します。

Twitterとかにコメントや突っ込みお願いします。

簡単な型

型が簡単な関数なら普通に_importで済みます。

val puts = _import "puts": string -> ()
val () = puts "Hello, C"

尚、簡単な型とは公式ドキュメントにある通り、いわゆる即値、即値の組(タプル)、即値の配列、即値の参照、それらを引数、返り値に持つ関数などです。

又、以下のような制約もあります。

C関数全体の型は,引数リストを組型とするMLの関数型に対応付けられます. ただし,C関数の引数や返り値に書ける相互運用型には,以下の制約があり ます. 配列型や組型など,Cのポインタ型に対応する相互運用型を,C関数の返り値の 型として指定することはできません.

恐らくGCとの兼ね合いでしょうがつらいですね。stringすら返り値で受け取れません。

それにこの制約がどこまで効いてるのかが不明で、同じ型でも型付けに成功したり失敗したりすることがあります。例えば上の例でstring型を引数にとる関数をインポートしましたが関数に依ってはstringが相互運用型でないとか怒られることがあります。タプルの配列やタプルの参照などは確実にダメみたいです。

尚、string型はCでいう const char * 、タプルは変更不能な構造体へのポインタになるそうです。構造体の即値は扱えないんですね…。また、参照とは別に ptr 型も存在します。SML#側からは作れず、Cとの相互運用のためだけに存在するようです。

魔法の unit ptr

じゃあ複雑な型はインポート出来ないのかというとそうでもなく、 unit ptr 型にしておけばとりあえずインポート出来ます。Cで言うところの void * です。 邪悪な雰囲気を感じますね。しかしそこは型安全言語、ちゃんと型安全に unit ptr を扱えます。

type file = unit ptr
val fopen = _import "fopen": (string, string) -> file
val fgetc = _import "fgetc": (file) -> int
val fclose = _import "fclose": (file) -> int
val () = let
      val f = fopen("test", "r")
      val c = fgetc(f)
    in
      print(str(chr c));
      fclose(f)
    end

はい。単に type で名前をつけてあげれば大丈夫です。SML#側ではポイント先が何であるかには関知せず、インポートしたC関数の間で完結してれば問題ありません。多くのライブラリはそのようなAPIになっているのではないでしょうか。

ポインタを扱う

とはいえ時にポインタを扱う必要もあります。構造体の配列を扱えないのでその辺で。

そういった時に便利なのが SMLSharp_Builtin.PointerPointer です。 Pointer の方は .smi ファイルの中で _require "ffi.smi" してから使います。

structure SMLSharp_Builtin
  structure Pointer =
  struct
    val identityEqual = _builtin val IdentityEqual : boxed * boxed -> bool
    val advance = _builtin val Ptr_advance : 'a ptr * int -> 'a ptr
    val deref = _builtin val Ptr_deref : 'a ptr -> 'a
    val store = _builtin val Ptr_store : 'a ptr * 'a -> unit

    val toUnitPtr = _builtin val Cast : 'a ptr -> unit ptr
    val fromUnitPtr = _builtin val Cast : unit ptr -> 'a ptr
    val toCodeptr = _builtin val BitCast : unit ptr -> codeptr
  end
  end

structure Pointer =
struct
  val advance = SMLSharp_Builtin.Pointer.advance

  val load =
      case 'a in 'a ptr -> 'a of
        int => SMLSharp_Builtin.Pointer.deref
      | word => SMLSharp_Builtin.Pointer.deref
      | SMLSharp_Builtin.Word8.word => SMLSharp_Builtin.Pointer.deref
      | char => SMLSharp_Builtin.Pointer.deref
      | real => SMLSharp_Builtin.Pointer.deref
      | SMLSharp_Builtin.Real32.real => SMLSharp_Builtin.Pointer.deref
      | 'b ptr => SMLSharp_Builtin.Pointer.deref

  val store =
      case 'a in 'a ptr * 'a -> unit of
        int => SMLSharp_Builtin.Pointer.store
      | word => SMLSharp_Builtin.Pointer.store
      | SMLSharp_Builtin.Word8.word => SMLSharp_Builtin.Pointer.store
      | char => SMLSharp_Builtin.Pointer.store
      | real => SMLSharp_Builtin.Pointer.store
      | SMLSharp_Builtin.Real32.real => SMLSharp_Builtin.Pointer.store
      | 'b ptr => SMLSharp_Builtin.Pointer.store

  val isNull : 'a ptr -> bool
  val NULL : unit -> 'a ptr

  val importBytes : SMLSharp_Builtin.Word8.word ptr * int
                    -> SMLSharp_Builtin.Word8.word vector
  val importString : char ptr -> string
end

loadstorederefadvance あたりを良く使いそうですね。

実際にあった話。 struct header { const char *name; int name_len; const char *value; int value_len} の配列(struct header *)を扱う必要がありました。 その配列をCの関数に渡して書き換えてもらって、後で値を取り出したいという状況がです。その時値を取り出すコードがこれです。

fun getHeader headers i =
      let
          val header_ptr : char ptr ptr = fromUnitPtr(headers)
          val header_ptr = advance(header_ptr, i * 2)
          val header_ptr : int ptr =
              fromUnitPtr(toUnitPtr(header_ptr))
          val header_ptr = advance(header_ptr , i * 2)

          val header_ptr : char ptr ptr =
              fromUnitPtr(toUnitPtr(header_ptr))
          val name = deref(header_ptr)
          val header_ptr = advance(header_ptr, 1)

          val header_ptr : int ptr =
              fromUnitPtr(toUnitPtr(header_ptr))
          val nameLen = deref(header_ptr)
          val header_ptr = advance(header_ptr, 1)

          val header_ptr : char ptr ptr =
              fromUnitPtr(toUnitPtr(header_ptr))
          val value = deref(header_ptr)
          val header_ptr = advance(header_ptr, 1)

          val header_ptr : int ptr =
              fromUnitPtr(toUnitPtr(header_ptr))
          val valueLen = deref(header_ptr)
      in
          if isNull name
          then (NONE, String.substring(importString(value), 0, valueLen))
          else (SOME(String.substring(importString(name), 0, nameLen)),
                String.substring(importString(value), 0, valueLen))
      end

まず、タプルは構造体へのポインタなので今回の struct header *(string * int * string * int) ptr ではありません。それは struct header ** になってしまいます。 また、ポインタを扱う関数が ptr 型しか受け付けないので string ではなく char ptr にしておいて後から importString で文字列にする戦略をとります。

そして配列のi番目にアクセスしたかったら先述の通り (string * int * string * int) ptr ではないので地道に char ptr ptr 2i個分、 int ptr 2i個分ポインタを進めます。 ポインタの型を変える時はダイレクトには変換出来ないようなので一旦 unit ptr を経由してから変換。そして次のメンバにアクセスするために advance という形をとります。

そこまでしたら後は deref してあげれば欲しい値がとれます。

REPLからのimportと DynamicLink

SML#のREPLからも勿論インポート出来ますが、SML#のランタイムにリンクされてないライブラリのものはインポート出来ないのでダイナミックリンクを使います。 DynamicLink にCの dl* と同じ関数群が用意されているのでそれらを使います。

structure DynamicLink =
struct
  type lib (= ptr)
  datatype scope = LOCAL | GLOBAL
  datatype mode = LAZY | NOW
  val dlopen : string -> lib
  val dlopen' : string * scope * mode -> lib
  val dlsym : lib * string -> codeptr
  val dlsym' : lib * string -> unit ptr
  val dlclose : lib -> unit
end

val lib = dlopen("libawsome.so") でライブラリのオープン、 dlsym(lib, "awm_function"): _import () ->unit で読み込みです。

これでインポートする関数は必要になった時に読み込んで欲しいのですがトップレベルで val でバインドすると即読み込まれてしまいます。その辺を上手くやるテクニックがSML#のソースにありました。MySQLのバインディングの部分です。

  fun lazy f =
      let
        val r = ref NONE
      in
        fn () =>
           case !r of
             SOME x => x
           | NONE =>
             let val x = f ()
             in r := SOME x; x
             end
      end

  val lib =
      lazy (fn _ =>
               DynamicLink.dlopen
                 (case OS.Process.getEnv "SMLSHARP_LIBMYSQLCLIENT" of
                    NONE => "libmysqlclient.16." ^ SMLSharp_Config.DLLEXT ()
                  | SOME x => x))

  fun find s = DynamicLink.dlsym(lib (), s)
  val mysql_init =
      lazy (fn _ => find "mysql_init"
                    : _import (MYSQL) -> MYSQL)


...

遅延評価してますね。これ。呼び出す時は mysql_init () (mysql) みたいに一旦lazyを剥がさないといけないので注意です。

問題とか

Cの仕様

確かCの仕様上構造体のメモリ上の表現にはメンバ以外のものも置いていいことになっていた筈です。 上の方法では変なコンパイラでコンパイルしたコードだと動きそうにないですね。GCCやClangは大丈夫な筈。

そもそもSML#自体GCCとABI互換なコンパイラでコンパイルしたものじゃないとリンク出来なそうな気がするので杞憂ですかね。

メモリ確保

Cの関数から構造体のポインタが返ってくるケースだと良いんですが自分で構造体を用意してCの関数に渡すケースだとメモリの確保が問題になります。現状、

struct header
*prepare_headers(int n)
{
  return malloc(n * sizeof(struct header));
}

みたいなヘルパ関数を用意して凌いでますが欲しい構造体毎に書かないといけないのであまり嬉しくないです。 sizeof をSML#側でとれれば単に malloc をバインドするだけで済むのに。 もう少し欲を言うと malloc したらGCから外れそうな気がするので明示的に free する必要がありそうです。GCに載るメモリ確保関数も欲しいですね。 さらに欲を言うとスタックアロケートする版のメモリ確保関数も欲しい。もしかしたら alloca で大丈夫なんでしょうか。

#define

ヘッダファイル内で定数を #define してあることが多々あります。それらは地道に手書きでSML#側に持ってくることになりますが気になるのが互換性の問題。 特にOSのヘッダファイルには名前は同じだけどOS毎に値が異なるものが存在します。シグナルとか。OSで条件分岐するか理想的にはプリプロセッサのサポートがあればどうにかなりそうなんですけどねぇ。 現状だとCで定数を返すgetter関数を書いてSML#側でインポートして…ってやればどうにか出来そうですけどやりたくないですね。

変数

私はまだ遭遇してないのですがライブラリによってはグローバル変数にアクセスしないといけないものが存在します。これもgetterとsetterを書いて…ってやるとどうにか出来そうですがどうせなら変数のインポートも出来ると良いですよね。

まとめ

  • SML#でCのバインドを書く時は少しテクニックが必要
  • SML#にはポインタを直接扱える関数もある
  • それでも機能が足りない時はCでヘルパ関数を書こう。
  • ダイナミックリンクライブラリも扱えるよ
Written by κeen