【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

FFI (C++/CLI)

FFIC言語を呼び出せたので、extern "C"でラップした.NET APIを呼び出せるか試しました。Windows FormsでGUIが表示できました。右がスクリーンショットで、以下がソースです。

gccではC++/CLIを扱えないため、DLLはVisual C++ 2010 Express Editionで作成しました。今回はテスト目的のためラッパーを自作しましたが、hs-dotnetという汎用ラッパーが存在します。

今回は使いませんが、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では関数名は必ず小文字で始めないといけません。これを知らずに大文字始まりで関数をインポートして、謎のエラーでハマりました。

感想

苦労して手動でマーシャリングすることで、ようやくHaskellから.NET APIを呼び出すことができました。こんな調子で全部ラッピングするのは現実的ではないので、冒頭で紹介したhs-dotnetを使うのが現実的だと思います。

それ以前の問題として、Haskellから.NETを使うこと自体が現実的ではないという気もしますが。