LoginSignup
5

More than 5 years have passed since last update.

C++/CLI でEventHandler/関数ポインタ変換用の便利クラスを作る

Last updated at Posted at 2016-06-12

C++/CLI でコールバック用関数ポインタの生成、およびそれをC#で定義された EventHandler へ購読させるノート。

  1. native DLL はコールバック関数を渡すようなAPIとなっている。
    イベントが発生したら、そのコールバックにデータが渡される。
  2. 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)を用意します。イカを参考にしました。

コールバックと型宣言

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 してしまう方が、楽な気がします。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5