OCamlのライブラリをコンパイルする正しい方法
もしOCamlのライブラリを拾ってきても、原則として添付のconfigure/Makefileは信用してはダメです。自分でコンパイルしましょう。さもないと後々トラブルに見舞われることでしょう。OCamlのライブラリ界隈はそんな感じです。
ここでは、既存のlibnice.aを使うためのOCamlのインターフェースライブラリとしてnice_stub.c, nice.mli, nice.mlが存在するとします。
Windows以外
C側のソースをコンパイルします。nice_stub.cを一応覗いてみて、中に#ifdefが書かれていた場合、必要なものを-Dで渡します。
gcc -isystem `ocamlc -where` -c -O2 nice_stub.c
OCaml側のソースをコンパイルします。OCamlには条件コンパイルなんて無いので楽です。
ocamlc -c nice.mli ocamlc -c nice.ml ocamlopt -c nice.ml
ocamlmklibを使って必要なファイルを作ります。
ocamlmklib nice_stub.o nice.cmo nice.cmx -o nice -oc mlnice -lnice
-ocでスタブの名前を変えているのは、そのままですとnice_stub.oからlibnice.aが作られて、既存のlibnice.aと被るからです。
またリンクするライブラリには決してパスを指定してはいけません。パスは-Lで指定してください。
これでdllmlnice.so, libmlnice.a, nice.cma, nice.cmxa, nice.aが作られます。
後はsudo cpするだけです。
dllmlnice.soを/usr/local/lib/ocaml/stublibs(`ocamlc -where`/stublibs)にコピーします。
libmlnice.a, nice.cma, nice.cmxa, nice.aと、nice.cmiを/usr/local/lib/ocaml(`ocamlc -where`)にコピーします。
G'Camlの場合は一旦nice_stub.o以外を消して、oをgに変えたコマンドを繰り返します。C側ヘッダーにはOCamlとG'Camlの違いは無いためnice_stub.oはコンパイルし直す必要はありません。ただしG'Camlはoptコンパイラを作れないのでopt関係は省きます。gcamlmklibがnice.cmxが無いぞと文句をいいますがバイトコードで使うために必要なファイルは作られますので無視してOKです。
gcamlc -c nice.mli gcamlc -c nice.ml gcamlmklib nice_stub.o nice.cmo -o nice -oc mlnice -lnice
dllmlnice.soを/usr/local/lib/gcaml/stublibs(`gcamlc -where`/stublibs)にコピーします。
nice.cmaと、nice.cmiを/usr/local/lib/gcaml(`gcamlc -where`)にコピーします。
OSXの場合でlibnice.aではなくlibnice.dylibの場合、リンク時にcyclicなんとか言われたら、-dylib_fileを追加します。-dylib_fileはダイナミックリンクするファイル名を明示するオプションです。同じファイル名をコロンで区切って2回繰り返します。*1
ocamlmklib nice_stub.o nice.cmo nice.cmx -o nice -oc mlnice -lnice -cclib "-dylib_file /fullpath/libnice.dylib:/fullpath/libnice.dylib"
ただし、-dylib_fileを使う必要があるものや、SDLのようなmain乗っ取り型ライブラリは、当然ocamlrunに影響を及ぼすことができないためバイトコードでは使えません……ocamlmktopしたらいけるかな?
Windows以外ですと、作者様が添付してくれているMakefileでも問題ないことは多いと思います。Windowsが問題なのです。おそらくこのページは、WindowsでのOCamlの正しいライブラリの作り方を解説した唯一のページになることでしょう……。
ocamlc -c nice.mli ocamlc -c nice.ml ocamlopt -c nice.ml
C側のコンパイルは……misc.hを見ていただければわかりますが、.dllか否かで条件コンパイル切られているのはWindowsの時のみです。従ってWindowsでは、.dllを作る時と、.aを作る時で、.cをコンパイルし直す必要があります。
#if defined(_WIN32) && defined(CAML_DLL)
幸いにして!?Windows版のOCamlにはocamlmklibが付いてこないため、ライブラリの構築は全て手動で行うことになります。これはビルドプロセスへの介入など厄介なことをしなくて済むようにとの配慮に違いありません。
とりあえず.cma/.cmxaを作るためにC側の実体が存在している必要は無いため、先に.cma/.cmxaを作っときましょう。
ocamlc -a -o nice.cma nice.cmo -dllib -lmlnice -cclib -lmlnice -cclib -lnice ocamlopt -a -o nice.cmxa nice.cmx -cclib -lmlnice -cclib -lnice
user32.dllやgdi32.dllその他のAPIが使用されている場合は、-cclib -luser32や-cclib -lgdi32も必要に応じて指定します。
-dllibではスタブのみを指定します。libnice.aはdllmlnice.dllにスタティックリンクしてしまえばいいですし、もしnice.dllとして提供されている場合でもdllmlnice.dllがロード(インポートライブラリ or LoadLibrary)するように作れば-dllibに指定する必要はありません。むしろ指定してはダメです。後述しますが、-dllibにはflexlinkでリンクしたOCamlのバイトコード用のスタブのみを指定しないと不具合が出ます。
さてC側です。
まず.aのほうを作りましょう。MinGW版を使います。VC++版をお使いの方は適当に読み替えてください。
gcc -c -O2 -isystem "C:\Program Files\Objective Caml\lib" nice_stub.c ar q libmlnice.a nice_stub.o
-where?改行コードのせいでそんなものは使えません。フルパス手書きです。
最後に、dllmlnice.dllを作るのですが……まずコンパイル。-DCAML_DLLを指定します。
gcc -c -O2 -isystem "C:\Program Files\Objective Caml\lib" -DCAML_DLL nice_stub.c
後は.dllをリンクするだけなのですが……http://alain.frisch.fr/flexdll.htmlからflexlinkをダウンロードしてきてください。ファイルは全部/mingw/binに入れておけばOKです。OCamlのスタブは、このflexlinkを使用してリンクしなければなりません。ちなみにFlexDLLの日本語記事が読めるのはYak!さんの日記だけ!(最初ここだけと書こうとして検索したら見つけてしまいましたorz)
flexlink -show-imports -noentry -chain mingw -o dllmlnice.dll nice_stub.o \ -L/mingw/lib \ -defaultlib /mingw/lib/libnice.a \ -defaultlib "C:\Program Files\Objective Caml\lib\ocamlrun.a"
UNIX系OSの.soは、持っているシンボルをプロセス内のモジュール全てに公開します。シンボル名だけで.so中の関数にアクセスできるわけです。Windowsの.dllは、シンボルは.dll毎に独立しています。flexlinkでリンクした.dllは、.soのように振舞う、というわけです。*2ここでリンクするライブラリの指定に-lではなく-defaultlibを使う理由は、-lですとocamlrun.aにあるシンボルまで公開されてしまうからです。
flexlinkを使わずにgcc -sharedやdlltoolでも、スタブ1個だけなら動作しますが、複数になると変な動きをします。flexlinkを使えばOKです。
ただし……それはあくまでocamlrun.exeから見ての話。flexlinkを使っても、スタブ間のシンボルの解決まではしてくれないのです。ここで-show-importsが重要です。例えば次のように表示されたとしましょう。
** Imported symbols for descriptor object: _caml_ba_alloc_dims
_caml_ba_alloc_dimsはbigarray.h/dllbigarray.dllにある関数です。
いかにも自動で_caml_ba_alloc_dimsを解決してくれるようなメッセージですが、解決してくれないです。_caml_ba_alloc_dimsを呼び出そうとするとアクセス違反で落ちます。
というわけでインポートライブラリを作ります。.defファイルが必要です。次の内容をdllbigarray.defとして保存してください。一般的には、objdumpや、Borlandのtdumpを使えば.dllが公開しているシンボルの一覧を得られますので、それを加工すればいいです。impdefで直接.defを吐かせてもいいですが、どの道アンダーラインの有無など識別子の修飾は弄る必要があります。
EXPORTS caml_ba_alloc caml_ba_alloc_dims caml_ba_blit caml_ba_create caml_ba_dim caml_ba_fill caml_ba_get_1 caml_ba_get_2 caml_ba_get_3 caml_ba_get_generic caml_ba_init caml_ba_kind caml_ba_layout caml_ba_map_file caml_ba_map_file_bytecode caml_ba_num_dims caml_ba_reshape caml_ba_set_1 caml_ba_set_2 caml_ba_set_3 caml_ba_set_generic caml_ba_slice caml_ba_sub
インポートライブラリの作成は古典的にdlltool。
dlltool -l dllbigarray.a -D dllbigarray.dll -d dllbigarray.def
作成したdllbigarray.aをflexlinkに渡せばOK。
flexlink -show-imports -noentry -chain mingw -o dllmlnice.dll nice_stub.o \ -L/mingw/lib \ -defaultlib /mingw/lib/libnice.a \ -defaultlib dllbigarray.a \ -defaultlib "C:\Program Files\Objective Caml\lib\ocamlrun.a"
インポートライブラリ作るの面倒だし別にコード重複してもいいよってlibbigarray.aを静的リンクしても、うまく動いてくれませんでした。なんかあるのでしょう。
このような手順を、"** Imported symbols for descriptor object:"が表示されなくなるまで繰り返せば、dllmlnice.dllは完成です。
スタブをC:\Program Files\Objective Caml\lib\stublibsに、それ以外をC:\Program Files\Objective Caml\libにコピーすれば、めでたしめでたしとなります。
G'Camlの場合も、oをgに変えてopt抜きでと特に変わったことはないです。ocamlrun.aがgcamlrun.aになる点に注意ください。
*1:コロンってOS9以前のパス区切り記号だったような……?
*2:各所ではまるでWindowsの.dllがまるでプラグイン用途に使えないように書かれていますが、それは違います。GetProcAddressがシンボル名とモジュールハンドルの両方を要求するだけですね。WindowsではCコンパイラだけで相当数ありますが、ランタイムの関数名が衝突したりしないのもこの.dllの仕様のおかげです。逆に言えば.so怖過ぎる。gccに並び立つコンパイラが出てきたら世界は崩壊するんじゃないでしょうか。さて、Windowsでシンボル名だけで関数を特定したいような場合でも、ロードしたdllを片っ端からGetProcAddressして回ればいいわけですから、flexlinkやedllのようなものを導入してしまうのはどうかと思います。