C++/CLI でコールバック用関数ポインタの生成、およびそれをC#で定義された EventHandler へ購読させるノート。
- native DLL はコールバック関数を渡すようなAPIとなっている。
イベントが発生したら、そのコールバックにデータが渡される。 - Csharp のインターフェイスコードは、普通のEventHander でイベントを公開するようなインターフェイス。
上記2つを結合させる部分のイベントハンドラー部分を実装するときのノート。
インターフェイス
以下のC宣言があるとする。
/** MyDLL */
/** コールバック定義
* @param [in] val 何か更新された値(適当な値)
* @param [in] lparam よくあるコールバックのタグ用のポインタ。
*/
typedef void (__stdcall* MyEventCallbackFunc)(int val, void* lparam);
/** 生成関数
* @param [in] init 何か適当な初期化用の値。
* @param [in] callback イベントが発生したら呼ばれるコールバック関数。
* @param [in] lparam callback を呼び出すときに渡すパラメータ。
* @return
*/
void* __stdcall MyCreateFunc(
int init,
MyEventCallbackFunc callback,
void* lparam);
/** 破棄関数
* @param [in] ptr MyCreateFunc() で返されたポインタ。
*/
void __stdcall MyDeleteFunc(void*);
/** 適当な操作
* @param [in] ptr MyCreateFunc() で返されたポインタ。
* @param [in] newValue 操作の引数。
*/
void __stdcall DoFunc(void* ptr, int newValue);
以下は、C#部分のインターフェイス。
// C#部分の宣言。
namespace pinky
{
/// <summary>
/// イベントデータ。
/// </summary>
public class MyEventData : EventArgs
{
public MyEventData(int val)
{
this.Value = val;
}
// 更新された値。
public int Value{get;private set;}
}
/// <summary>
/// 処理対象のインターフェイス。
/// </summary>
public interface IMyInterface
{
// 値が更新されたら、これで通知される。
event EventHandler<MyEventData> MyEventHandler;
// 処理するときのインターフェイス。
void DoFunc(int newValue);
}
}
Native 用のコールバック関数を C++/CLI で定義
まずは、コールバック用のクラス(C++/CLI)を用意します。イカを参考にしました。
-
Marshal.GetFunctionPointerForDelegate()
メソッドで managed の delegate を関数ポインタに変換する。 - C#のdelegateをCLI経由でC++の関数ポインタに渡す
コールバックと型宣言
public ref class MyCallback1{
public:
// native のコールバック型。
using NativeCallbackFunc_t = void(__stdcall*)(int val, void* lparam);
// NativeCallbackFunc_t に対応する、managed delegate
// 引数&戻りは、一致させること。
delegate void ManagedCallbackFunc(int val, IntPtr lparam);
// コールバック関数として呼び出されるメソッド。
void MyManagedCallback(int val, IntPtr lparam)
{
// ここに、EventHandler の呼び出しを置く。
}
};
Marshal::GetFunctionPointerForDelegate() でコールバック関数ポインタ生成
public ref class MyCallback1{
public:
// 省略
// ctor
MyEventHandler1()
{
// メンバー関数を、Delegateにする。
_eventHandlerDelegate= gcnew ManagedCallbackFunc(
this,
&MyEventHandler1::MyManagedCallback);
// delegate を、native の関数ポインタへ変換してもらう。
_funcPtr = Marshal::GetFunctionPointerForDelegate(
this->_eventHandlerDelegate);
}
// native 用のポインタ取得。
NativeCallbackFunc_t GetCallbackFuncPtr()
{
return static_cast<NativeCallbackFunc_t >( _funcPtr.ToPointer());
}
private:
/** コールバックデリゲードの本体。
作成後、GetFunctionPointerForDelegate() を利用して、これの関数ポインタを
_funcPtr へと渡されます。
*/
initonly EventHandlerDelegate^ _eventHandlerDelegate;
/** コールバック関数ポインタ、を保持するIntPtr
これを、Native 側にコールバック関数として渡します。
*/
System::IntPtr _funcPtr;
};
イベントハンドラの用意
public ref class MyCallback1{
public:
// 省略
// 省略2
// Callback の引数から、EventHandler 用データを作成するための、変換関数。
static MyEventData^ CreateEventArgFunc( int val, IntPtr lparam )
{
return gcnew MyEventData(val);
}
// 普通のイベントハンドラ型
using EventHandlerDelegate = System::EventHandler<MyEventData^>^;
event EventHandlerDelegate^ CallbackEventHandler
{
void add(EventHandlerDelegate^ eh){ _callbackEventHandler += eh; }
void remove(EventHandlerDelegate^ eh){ _callbackEventHandler -= eh; }
}
private:
EventHandlerDelegate^ _callbackEventHandler;
// "コールバック関数と型宣言" で出てきたメソッド
void MyManagedCallback(int val, IntPtr lparam)
{
auto fp = _callbackEventHandler;
if( fp == nullptr ) return;
auto arg = CreateEventArgFunc(val,lparam);
fp->Invoke(nullptr/*sender*/, arg);
}
};
IMyInterface の実装 ~MyCallback1 の使い方サンプル
使い方サンプルとしての、IMyInterface の実装です。☆1で、native の関数ポインタを渡します。
☆2で、EventHandler として公開します。
namespace pinky_impl{
public ref class MyImpl : public IMyInterface
{
// Native 関数用のポインタ
void* _ptr;
// さっき書いたコールバックオブジェクト
MyCallback1^ _callback;
public:
MyImpl(int init)
{
_callback = gcnew MyCallback1();
// ☆1 Native へは、このように関数ポインタを渡す。
_ptr = ::MyCreateFunc(init, _callback->GetCallbackFuncPtr(), nullptr);
}
virtual ~MyImpl(){this->!MyImpl();}
!MyImpl()
{
::MyDeleteFunc(_ptr);
}
public:
#pragma region IMyInterface 実装
// ☆2 イベントの add/remove 先を、コールバックオブジェクトにする。
event EventHandler<MyEventData^>^ MyEventHandler
{
void add(EventHandler<MyEventData^>^ eh)
{
_callback->CallbackEventHandler += eh;
}
void remove(EventHandler<MyEventData^>^ eh)
{
_callback->CallbackEventHandler -= eh;
}
}
// 普通のインターフェイス実装コード
void DoFunc(int newValue)
{
// native の方の関数へ転送。
::DoFunc(_ptr, newValue);
}
#pragma endregion IMyInterface 実装
};
}
TODO集
- Template で頑張る。(元実装はテンプレートだけど解説が面倒なので書きなおした。)
個人的にはこれのtemplate版で変換を楽にしてます。msさんが、C++/CLI 用のラムダを用意してさえくれたら完璧だったのに・・・。 - 戻り型のある関数のコールバックはどうしたらいいのか思いつかない。
- EventArgs はgenericにできる気がするけれど、コンパイラと心を通わせられなかったので諦めている。
- 自分の場合、NativeからManaged部分へ、面倒な型変換が必要だったので C++/CLI で実装しましたが、普通の型だけなら p/invoke だけで行ける気がします。
そのほかハマった箇所のノート
- template にすると include しないといけない。(CLIが認識できないのかな?)
- コールバック関数の呼び出しを終わったあたりで、身に覚えのないランタイムエラーが出たら、呼び出し規約を間違えていないか確認。
- native 側がまだ生きている状態で、コールバック用デリゲートを削除すると、アクセスバイオレーションで死ぬ。破棄順番を間違えないようにする。
- native のコールバック呼び出しで、なるべくなら、BeginInvoke など一旦制御をOSに返してから Managed のイベント応答処理を行うとよさげ。主に、Native側のモジュールの再入で参照カウンタ―の管理などが面倒にならなくていい。
これを書いておいてなんですが、ウィンドウズなら、WPFのウィンドウにも、何も考えずにPostMessage してしまう方が、楽な気がします。