FFIでC言語を呼び出せたので、extern "C"でラップした.NET APIを呼び出せるか試しました。Windows FormsでGUIが表示できました。右がスクリーンショットで、以下がソースです。
gccではC++/CLIを扱えないため、DLLはVisual C++ 2010 Express Editionで作成しました。今回はテスト目的のためラッパーを自作しましたが、hs-dotnetという汎用ラッパーが存在します。
- id:sirocco:20101026:1288085914
今回は使いませんが、HaskellとF#を連携させることもできそうです。
マーシャリング
マネージオブジェクトはpinで止めないとポインタが取り出せません。面倒なのでオブジェクトを配列に入れて、インデックスをハンドルとして扱うことにしました。このようなオブジェクトプールに入れるとGCが働かなくなりますが、今回は無視します。
ref class Globals { public: static List<Object ^> objs; }; template <typename T> T ^_get(int id) { return dynamic_cast<T ^>(Globals::objs[id]); } inline int _add(Object ^obj) { int ret = Globals::objs.Count; Globals::objs.Add(obj); return ret; } #define CEXPORT extern "C" __declspec(dllexport) CEXPORT int new_Form() { return _add(gcnew Form()); } CEXPORT int new_Button() { return _add(gcnew Button()); }
関数ポインタ
.NETからHaskellの関数にコールバックさせるため、三重にラップする羽目になりました。もっと効率の良い方法があるかもしれません。
- Haskell関数 → 関数ポインタ(FunPtr) → Handler → EventHandler
まずHaskell側でFFIの機能を使って関数ポインタに変換します。"wrapper"というのはインポートする関数名ではなく、FFI組み込みの機能です。
foreign import ccall "wrapper" wrap :: IO () -> IO (FunPtr (IO ()))
C++/CLI側では渡された関数ポインタをdelegateでラップして保持する必要があります。関数ポインタを保持するクラスを定義して、呼び出し専用のメンバからEventHandlerを作成します。
ref class Handler { private: void(*f)(); public: Handler(void(*f)()) : f(f) {} void Invoke(Object ^sender, EventArgs ^e) { (*f)(); } }; inline EventHandler ^_wrap(void(*f)()) { return gcnew EventHandler(gcnew Handler(f), &Handler::Invoke); } CEXPORT void addClick(int c, void(*f)()) { _get<Control>(c)->Click += _wrap(f); }
FunPtr生成はIOモナドに包まれているため、一時変数を経由する必要があります。
fp <- wrap $ closeForm f addClick b2 fp
毎回一時変数を記述するのは面倒なので、別名でインポートしてラッパーで包みました。
foreign import ccall "addClick" c_addClick :: Handle -> FunPtr (IO ()) -> IO () addClick c f = do fp <- wrap f; c_addClick c fp
これを使えば見た目が自然になります。FunPtrは明示的に解放しないとリークしますが、今回は無視します。
addClick b2 $ closeForm f
文字列
文字列も複雑で二重に変換しています。Haskellの文字列はCharのリストなので、ワイド文字列に変換してからStringに変換しています。
- [Char] → CWString = wchar_t[] → String
CWStringもFunPtr同様に後始末が必要です。コールバックと異なり寿命が呼び出し完了までと分かっているため、RAII的に片付けてくれるwithCWStringを使うと簡単です。HaskellのソースをUTF-8で書けば日本語もOKです。
foreign import ccall "setText" setText :: Handle -> CWString -> IO () withCWString "あいうえお" $ setText f
CEXPORT void setText(int c, const wchar_t *text) { _get<Control>(c)->Text = gcnew String(text); }
引数が増えるとwithCWStringの記述が煩雑になるため、ラッパーを用意しました。
foreign import ccall "msgbox" c_msgbox :: CWString -> CWString -> IO () msgbox a b = withCWString a $ \wa -> withCWString b $ c_msgbox wa msgbox "こんにちは" "世界"
CEXPORT void msgbox(const wchar_t *text, const wchar_t *title) { MessageBox::Show(gcnew String(text), gcnew String(title)); }
関数名
Haskellでは関数名は必ず小文字で始めないといけません。これを知らずに大文字始まりで関数をインポートして、謎のエラーでハマりました。