SML# - C言語バインディング まとめ (ML/LL AdventCalendar20日目)
この記事は LL/ML Advent Calendar #LLAdventJP 20012/12/20の参加記事です。
出来心でRTした次の瞬間には犠牲に参加することになっていました。なごやこわい。
注意:この記事では SML#1.2.0 を使うものとしています。
SML# にはC言語バインディングをサポートする機構が言語レベルで組み込まれており、簡単にC言語で書かれたライブラリを利用することが出来ます。
しかしまぁ、簡単とは言っても相手はあのC言語ですよ。
現実世界のライブラリをバインドするにはキレイな世界だけで完結するはずも無いのです…。
ここでは、私がまともに使い始めて分かってきたことを含めて、基礎からSML#のC言語バインディングの機能を一通り紹介します。
いきなり例
もう界隈の人にはおなじみかもしれません。最も簡単なバインディングの例です。
# val puts = _import "puts" : string -> unit # puts "hogehoge\n"; hogehoge
はい。簡単ですね。_importと型式を書くだけでC言語の関数を呼び出せます。
型の対応関係
もうバインディングの文法は分かったので、C言語型、SML型、importに指定する型の対応関係を一覧してみます。
importの型式 | SML#の型 | C言語の型 |
---|---|---|
int | int | int |
string | string | char const * |
Real32.real | Real32.real | float |
real | real(Real64.real) | double |
'a ptr | 'a ptr | 'a * (任意型のポインタ) |
'a ref | 'a ref | 'a * (プリミティブ型のポインタ) |
'a vector | 'a vector | 'a const * |
'a array | 'a array | 'a * |
() -> 'b | unit -> 'b | 'b (*)(void) |
'a -> 'b | 'a -> 'b | 'b (*)('a) |
('a, 'b) -> 'c | ('a * 'b) -> 'c | 'c (*)('a, 'b) |
念のため述べておくと、SMLでは string は char array とは異なる型です。*1
どちらか適切な方を選びましょう。
arrayとvectorはそれぞれ、標準で用意されているmutable/immutableな配列です。
importの使い方
文法としてimport文の使い方と、上の対応表で示したimport文の型式を説明します。
import文の構文は非常に単純で以下のように表せます。
_import "シンボル名" : <import用の型式>
シンボル名としてライブラリが公開している名前を指定します。
型式としては、ほぼSMLのそれと同様に記述できますが細かい違いがあります。ここに書くのはあくまで *import文のための型式* であることに注意して下さい。
例えば以下の関数をバインドする場合、
void f0 (void); void f1 (char const *); void f2 (char const * , int);
次のようにしてバインドします。
val f0 = _import "f0" : () -> unit val f1 = _import "f1" : string -> unit val f2 = _import "f2" : (string, int) -> unit
一見普通なのですが(ML的に)普通じゃないです。*2引数型に注目してください。voidが引数の場合、import文では unitでは無く () を指定してます。
f2の引数型も string * int ではなく、(string, int) となっています。バインドした関数f2の型は string * int -> unit となります。
後述しますがimport文でタプルを指定すると、構造体へのポインタを引数に取る関数として扱われてしまいます。
戻り型の方は常に型と同じ表記で大丈夫です。この例ではvoidとunitが対応しています。
複合型
プリミティブについては分かりました。構造体ではどうなるでしょうか?
次のような intのメンバを2つ持つ構造体と、それへのポインタを受け取る関数はそれぞれ、タプルとそれを受け取る関数になります。
typedef struct { int x; int y; } S; void printS (S const * s) { printf ("S{%d,%d}", s->x, s->y); }
これを以下のようにバインドします。型式にタプルを指定していることに注目してください。
val printS = _import "printS" : (int * int) -> unit val s = (5, 723) val _ = printS s (* "S{5,723}" *)
注意すべきなのは、(構造体への)ポインタがタプルに対応することです。*3
さらに別の注意点として、タプルはSMLレベルでは普通の値(つまり当然immutable)ですが、C言語ではポインタを受け取ると言うことは…
void plus_assign(S * s) { s->x += s->y; }
val plus_assign = _import "plus_assign" : (int * int) -> unit val s = (72, 26) val _ = plus_assign s val _ = printS s (* "S{98,26}" *)
このように、タプルの値がC言語の関数によって勝手に書き換わりえます。注意してください。
_ffiapplyで型のサイズを得る
C言語の関数は型の大きさ(つまりsizeof式の値)を引数に要求することがあります。
SML#にはsizeofはありませんが、_ffiapply構文の中でのみ _sizeof ('a) という構文で型のサイズを得ることが出来ます。
次のコードは qsort をバインドする例です。(サンプルの場所)
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
それからSML#です。_sizeofが第3引数に使われています。
# fun sort ord xs = _ffiapply _import "qsort" ( xs :'a array , Array.length xs:int , _sizeof('a) , ord:('a ptr,'a ptr) -> int) : unit; val sort = fn : ['a. ('a ptr * 'a ptr -> int) -> 'a array -> unit]
シグネチャを見た通り、SML側から普通のarrayを渡すとarrayの長さと型引数のサイズを自動的にC言語の関数に渡してくれます。
第4引数では地味に高階関数が使えてますね。('a ptr, 'a ptr) -> int と言う式をみて分かるように、ここにはimport文と同じ規則で型を指定します。
一応、実際に使っている様子も書いておきましょう。
# val xs = Array.fromList [3,2,8,9,0,1,4]; (* 配列を作る *) val xs = <3, 2, 8, 9, 0, 1, 4> : int array # sort (fn (x,y)=> P.load x - P.load y) xs; (* ソートに渡す *) val it = () : unit # xs; val it = <0, 1, 2, 3, 4, 8, 9> : int array (* ソートできた *)
(o^-')b
実践的なテクニック
ここからは文法や機能の話でなく、私が使っていて分かってきたテクニックや注意点をつれづれ書きます。
ポインタの操作各種
まずは手札の確認から。 smlsharp-1.2.0/src/ffi/main/Pointer.sml にポインタの基本的な操作がそろってます。
structure Pointer = struct val NULL = fn : ['a. unit -> 'a ptr] val advance = <builtin> : ['a. 'a ptr * int -> 'a ptr] val importBytes = fn : word8 ptr * int -> string val importString = fn : char ptr -> string val isNull = fn : ['a. 'a ptr -> bool] val load : ['a::{int, word, word8, char, real, real32, 'b ptr}, 'b. 'a ptr -> 'a] val store : ['a::{int, word, word8, char, real, real32, 'b ptr}, 'b. 'a ptr * 'a -> unit] end
これに加えて以下の関数、
structure SMLSharp.Pointer = val fromUnitPtr = <builtin> : ['a. unit ptr -> 'a ptr] val toUnitPtr = <builtin> : ['a. 'a ptr -> unit ptr] end
辺りも頭に入れておくとよいと思います。ところでなぜNULLは関数なんですかね?
hoge_init関数
なぜかC言語のライブラリは void _init(int argc, char ** argv) のような関数が好きです。init()。
ライブラリをバインドしようとしていきなり char** とかを見て「char ptr ptrの値とかどうやって作るんだ。。。死のう。。。」みたいに諦める感じになるのは残念なのでここで取り上げておきます。((ヾノ・∀・`)ナイナイ
(* GLUT *) void glutInit(int *argcp, char **argv); (* Gtk+ *) void gtk_init(int *argc, char ***argv);
これらはmain関数の引数が渡されることを期待しているので、"文字列の配列(へのポインタ)" を渡せばよいことになります。
val glutInit = _import "glutInit" : (int ref, string array) -> unit val gtk_init = _import "gtk_init" : (int ref, string array ref) -> unit
SMLにはmain関数とか(Startとか)ありませんが、コマンドライン引数を取得する方法は標準の CommandLine モジュールで提供されています。
当然文字列のリストです。
これを使って初期化関数を呼び出します。
let val args = Array.fromList (CommandLine.arguments()) val argc = ref (Array.length args); in glutInit (argc, args) または gtk_init (argc, ref args) end
gtk_initを試したところ、マニュアルには明示されてないですが、Gtk+が認識する引数を渡しても引数は書き換えないようです。
glutInitの方は未確認ですが、マニュアルに書き換えると書いてあるので注意した方がよさそうです。
ゆるふわキャストライブラリ。型って何だっけ???
C言語では void* のような型の他に関数ポインタを適当にキャストする文化が一部に(?)あるようです。例としてまたGtk+です。
Gtk+で(多分)よく使う関数にg_signal_connect_dataがあります。
gulong g_signal_connect_data (gpointer instance, const gchar *detailed_signal, GCallback c_handler, /* これがヤバイ */ gpointer data, GClosureNotify destroy_data, GConnectFlags connect_flags);
これは、Gtk+のウィンドウなどにシグナルハンドラ(コールバック)を登録する関数で、
第1引数にウィンドウ、第2引数にシグナル、第3引数に関数ポインタを渡します。ユーザがハンドラに渡したいデータは第4引数で指定します。
さて、GCallbackはどういう型かというと……… void(*)(void) です。つまりどんな型でもキャストしてvoid(*)(void)にして渡します。
ここに渡される関数の型は反応するシグナルの種類によって以下のように何種類もあります。(これで全種類というわけではない)
bool f0 (GtkWidget *, GdkEventKey *, gpointer); void f1 (GtkWidget *, void *); void f2 (GtkWidget *, string, gpointer); void f3 (GtkWidget *, string, string, gpointer); /* シグナルの種類によってどの型の関数を渡せるか決まる */ g_signal_connect_data (x, sig0, f0, ...); g_signal_connect_data (x, sig1, f1, ...); g_signal_connect_data (x, sig2, f2, ...); g_signal_connect_data (x, sig3, f3, ...);
( ゚д゚) ・・・
(つд⊂)ゴシゴシ
(;゚д゚) ・・・
(つд⊂)ゴシゴシゴシ
(;゚Д゚) …!? (実際目を疑いました)
こういう場合、開発者をぬっころs同じ関数を異なる型で複数回importして凌ぐしか無さそうです。
datatype ('a, 'b) Signal = Event of 'a ptr * unit ptr * 'b ptr -> gboolean | Signal of 'a ptr * 'b ptr -> unit | Toggle of 'a ptr * string * 'b ptr -> unit | Edited of 'a ptr * string * string * 'b ptr -> unit val signal_connect_data_signal = _import "g_signal_connect_data" : ( 'a ptr , string , (('b ptr, 'c ptr) -> unit) , 'c ptr , (('d ptr, GClosure ptr) -> unit) , GConnectFlags) -> GTypes.gulong val signal_connect_data_event = _import "g_signal_connect_data" : ...それぞれの型を指定 val signal_connect_data_toggle = _import "g_signal_connect_data" : ... val signal_connect_data_edited = _import "g_signal_connect_data" : ...
文字列が戻ってくる
constな文字列(=どこかの文字列への参照)が戻ってくる場合があります。
char const * name (void);
が、stringを戻り型にバインドすることは出来ませんので、Pointerモジュールを使って文字列を取り出します。
(別に一行でやる必要性は無いんですけど)
fun f () = Pointer.importString (_import "name" : () -> char ptr)
void*を要求する関数ポインタは相手にしない
上の例でも出てきましたが、C言語で高階関数を使う場合、その関数にユーザの任意のデータを与えさせたい場合はほとんど void*(Gtk+ではgpointerという名前になってる) にせざるを得ません。
ですのでSMLにバインドするときにも unit ptr のような型になります。
後半の導入で紹介した通り、組み込み型ならばptr型でもどうにかなります(toUnitPtrとかimportStringとか)。
ですが、任意型の値のポインタを作ることは出来ません。こういうときはC言語を相手にするのを止めて普通にクロージャを使うとシアワセです。
例として、上で紹介したシグナルハンドラを使います。
fun callback str = Signal (fn _=> Msg.print (concat["Hello again - ", str, " was pressed\n"])) val _ = Sig.signal_connect (button, "clicked", callback "button 1", P.NULL())
ここではボタンをクリックするとコンソールにメッセージが表示される関数をシグナルハンドラとして登録しています。
ポイントは関数callbackがstrを引数に取った後、シグナルハンドラを構築していることです。つまり普通のクロージャなのでSMLの任意の型と式がC言語側から呼ばれるコールバック内で使えます。
カリー化した関数でも普通にC言語側に渡せるので、むしろC言語をそのまま使うより便利になっています。素晴らしいですね。
ちなみにstring -> char ptrみたいな関数を書こうとすると恐らくハマります。
タプルかポインタか
構造体をimportすると、タプルになるというのは既に紹介しました。なんとなくptr型よりもメンバが見えていた方がカッコイイ気がするのですが、デメリットもあります。
まずタプルはNULLに出来ません。引数を要求する側にNULLを指定する必要がある場合はptr型である必要があります。
次に、以下のようなシグネチャの関数を考えます。
val f = _import "f" : (int, (int * int) -> int) -> unit
これは一見良さそうですが、コンパイル出来ません。構造体を受け取る機能は、トップレベルの関数の引数の時だけです。
従って、その構造体のポインタを受け取るコールバックがライブラリで提供されているかを気に掛ける必要があります。
opaqueタイプのポインタを扱う
これまで見てきたとおり、C言語のポインタ型は'a ptr型としてバインドすることが出来ます。
val f = _import "f" : ('a ptr, 'a ptr) -> int (* 'a に何が渡されようとどうせポインタなので大丈夫 *)
従って実装を隠蔽した型のポインタ型であっても(!)バインド出来るはずです。が、実際にやってみるとコンパイルエラーとなります。
これは例えば、unit型に別名を付けて実装を隠蔽したモジュールが提供する型を使いたい場合に遭遇します。…例を見た方が速いです。
(* A.smi *) structure A = struct type Widget (= unit) (* .smiファイルで実装を隠蔽している *) end (* B.sml *) structure W = struct val show = _import "gtk_widget_show" : A.Widget ptr -> unit (* 実装が見えないためインポート出来ない *) end
このような場合、A.Widget型の定義が見えないためコンパイルエラーとなってしまいます。当然、モジュールAの型定義を公開したくはありません。
こういった時はシグネチャ側で適切に制約を加えればうまく行きます。
(* B.sml *) structure W : sig val show : A.Widget ptr -> unit (* シグネチャで本来の意図通り制約する。smiにもこれと同じ様に書く。 *) end = struct val show = _import "gtk_widget_show" : 'a ptr -> unit (* 多相ポインタなのでうまく行く *) end
ちょっと怪しいですが手元では動いています。大丈夫でしょう。多分。
importを書いたモジュール内だと誤用しそうと思うかも知れませんが、そもそもバインディングするモジュールはバインディング専用モジュールにして切り出しておくのがよいと思いますのでほとんどの場合問題無いと思います。
参考
- SML#のソースコードに同梱されているサンプル
- SML#の拡張機能:Cとの直接連携
- SML#がサポートする相互運用型