アンマネージコードにC#のデリゲートを渡す

今回はC#のトピックスの紹介です。
内容はタイトル通りアンマネージコードにC#レベルのデリゲートを渡す方法です。


まず前提知識として、アンマネージコードとはWin32APIの関数だとか自分で書いたC,C++のコードを指します。
C#を使っていても、なんだかんだでアンマネージコードを呼び出す機会はあるわけで、
そしてその関数がコールバック関数を要求することもたまーにあります。


そういう場合C#の側では、コールバック関数のシグネチャに対応するシグネチャでデリゲートを定義し、
そのデリゲートを渡すことによってコールバック関数を実現します。


たとえばWin32APIには

BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam );

という関数があります。
この第一引数のWNDENUMPROCはコールバック関数なので、C#からこの関数を呼び出したい場合はC#からデリゲートを渡してあげないといけません。
なのでWNDENUMPROCのシグネチャを調べ、それに対応するC#のデリゲートを定義してあげます。

まずWNDENUMPROCの定義は
BOOL CALLBACK EnumWindowsProc(HWND hwnd,LPARAM lParam);
それに対応するデリゲートは
delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
になると思います。

ついでにEnumWindowsのDllImportも書いておきます。

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool EnumWindows([MarshalAs(UnmanagedType.FunctionPtr)] EnumWindowsProc lpEnumFunc, IntPtr lParam);

このとき引数のデリゲートに属性
[MarshalAs(UnmanagedType.FunctionPtr)]
をつけることをお勧めします。
DllImportの場合自動的に判断されてうまいこと渡されるのですが、明示的に書いておいたほうがわかりやすいと思います。
そして重要なのがComImportしたインタフェースの中にデリゲートを渡す必要のあるメソッドがある場合、先述の属性が必ず必要になります。
ないと悲しいことになります。というかなりました。皆さん僕の二の舞にはならないように。。


ここまではWin32APIのようにあらかじめ用意されている関数に対してデリゲートを渡す方法です。
ほかにも自分でアンマネージコードの関数を作りそいつがコールバック受け取るようにももちろんできます。


できますがその際気をつけなければならないことがあります。


アンマネージコードにとってコールバック関数を受け取るということはつまり関数ポインタを引数にとるということです。
たとえば戻り値intで引数もintを一つ受け取る関数の関数ポインタの定義は普通に考えると

int(*pFunc)(int)

になると思います。
ですがこのように書いた関数ポインタに対して、上に書いたようにC#が対応するデリゲートを書いてアンマネージコードに渡し実行した場合、おそらく落ちます。


その理由は呼び出し規約にあります。
VCでは標準の呼び出し規約は__cdeclになっていると思います。
なので関数ポインタも__cdeclだと認識されます。
そして.netのメソッドはすべて__stdcallになっています。
呼び出し規約の違うものが渡されたので呼び出しちゃうとご臨終というわけです。


ではどうするか。僕は.netのメソッドを__cdeclにする方法を知りません。(※追記 デリゲートを__cdeclにする方法があるようです。詳細はコメント欄にあります。)
なので受け取る側であるアンマネージコードが合わせてやる必要があります。
その関数ポインタの書き方は次のようになります。

int(__stdcall *pFunc)(int)

__stdcallというキーワードがついただけです。これで実行はうまくいくはずです。
この__stdcallというキーワードをどこにつけるかを四苦八苦しながら探したということは秘密。


余談ですが、上に書いたWNDENUMPROCの定義にあるCALLBACKというものは実は単なる__stdcallのtypedefだったりします。
ほかにもおなじみWINAPIというのも同じく__stdcallのtypedefです。


これまでに書いたまでで、普通に使うには問題ありません。
しかしデリゲートをアンマネージコードに渡す場合もうひとつ気をつけなければならないことがあります。
むしろ今回の本題はそっちです。
つまり本題にまだ入ってないのですが、そろそろ長くなってきたので本題はまた次回に。