LoginSignup
15
12

More than 5 years have passed since last update.

C++/CLIラッパーでコールバックをリレーする

Last updated at Posted at 2016-11-24

概要

  • ネイティブC++ライブラリをC++/CLIでラップしてる人向け
  • C++/CLIやC#のdelegateを、ネイティブコード内のコールバックに突っ込む方法の解説
  • 対象者がすごく少なさそうだけど気にせずスタート

ネイティブ側の準備

コールバックの仕組みはどんな形態でもいいのですが、最終的にはvoid*を関数ポインタにキャストして実行することになるので、それを受け入れられるようになっている必要があります。
ここでは仮想関数のオーバーライドでコールバックを受け入れる実装を例に説明します。

class CallbackOwner {
  public:
    virtual void onCallback(void) {}
};

これを継承してonCallback()をオーバーライドすることで、任意の処理を突っ込めるようになっているものとします。これを関数ポインタベースにしたバージョンを作っておきます。

class FunctionOwner : public CallbackOwner {
  private:
    using callback_type = void(__stdcall *)(void);
    callback_type pCallback = nullptr;

  public:
    FunctionOwner(void* ptr)
    {
        pCallback = reinterpert_cast<callback_type>(ptr);
    }

    void onCallback() override
    {
        if (pCallback != nullptr) pCallback();
    }
};

マネージコードから受け取れる関数ポインタは呼び出し規約が__stdcallなので、それに合わせておく必要があります。ファンクタやラムダ式を使っている場合でも、やることは一緒です。

void*はコンストラクタ以外で渡しても良いのですが、コンストラクタで済ませた方がCLI側コードへの変更点が少なくて済むので、推奨しておきます。

以上でネイティブコード側の準備はバッチリです。

マネージコード側の仕込み

こちら側が色々と面倒です。一般的にCLIでラッパーを作る場合は、上記のクラスなら次のようにすると思います。(デストラクタやファイナライザは別途いいようにしておくとして)

namespace HogeWrapper {
    public ref class CallbackOwner {
      private:
        ::CallbackOwner* pUnamanaged;
      public:
        CallbackOwner()
        {
            pUnmanaged = new ::CallbackOwner();
        }
    }
}

ネイティブ側実装のインスタンスをポインタで保持し、CLI側からのAPI呼び出しをネイティブ側に委譲する……って、今思えばPImplイディオムそのものですね。
さて、ここからコールバックをリレーするまでに必要な手順ですが、

  1. マネージコード側からアタッチするためのpublic eventデリゲートを定義する
  2. 1.で定義したイベントを発火する関数と、それを関数ポインタに変換するためのデリゲートを定義する
  3. 2.で定義したデリゲートを関数ポインタに変換し、ネイティブ側インスタンスに渡す

となります。結構面倒な感じですが、1ステップずつ進めていきます。

イベントデリゲートの定義

デリゲートも普通の関数ポインタと同じように、型に対してエイリアスを作って使います。
引数なし、返値なしのデリゲートにActionCallbackという型名を付けて、メンバ名はOnCallbackというイベントを作ることにします。

namespace HogeWrapper {
    // 名前空間直下で定義しておいて
    public delegate void ActionCallback();

    public ref class CallbackOwner {
      private:
        ::CallbackOwner* pUnamanaged;
      public:
        CallbackOwner()
        {
            pUnmanaged = new ::CallbackOwner();
        }

        // 参照型としてメンバ名を付けて持たせる
        event ActionCallback^ OnCallback;
    }
}

イベント発火関数と関数ポインタ変換用デリゲートの定義

これでC#やC++/CLIから任意の処理を受け付けるようになったので、これをネイティブ側実装で呼ぶための発火関数を作ります。それを関数ポインタに変換するための内部デリゲートを用意し、発火関数を登録します。デリゲートを2つ使うのがポイントです。

namespace HogeWrapper {
    public delegate void ActionCallback();

    public ref class CallbackOwner {
      private:
        ::CallbackOwner* pUnamanaged;

        // 関数ポインタ変換用デリゲート
        ActionCallback^ innerCallback;

        // イベントを発火するだけ
        void FireCallback()
        {
            OnCallback();
        }

      public:
        CallbackOwner()
        {
            // イベント発火関数をデリゲートに登録
            innerCallback = gcnew ActionCallback(this, &CallbackOwner::FireCallback);
            pUnmanaged = new ::CallbackOwner();
        }

        event ActionCallback^ OnCallback;
    }
}

デリゲートを関数ポインタに変換し、ネイティブ側実装に渡す

デリゲートはMarshal::GetFunctionPointerForDelegate()で関数ポインタに変換できます。System::Runtime::InteropServices 名前空間のクラスなので、完全名で呼ぶとやたら長くなります。適当なスコープでusingしておくと良いでしょう。

namespace HogeWrapper {
    public delegate void ActionCallback();

    public ref class CallbackOwner {
      private:
        ::CallbackOwner* pUnamanaged;

        ActionCallback^ innerCallback;

        void FireCallback()
        {
            OnCallback();
        }

      public:
        CallbackOwner()
        {
            // Marshalの手前までusingしておく
            using namespace System::Runtime::InteropServices;

            innerCallback = gcnew ActionCallback(this, &CallbackOwner::FireCallback);

            // 関数ポインタに変換
            System::IntPtr ptr = Marshal::GetFunctionPointerForDelegate(innerCallback);

            // ネイティブ側実装にリレー
            pUnmanaged = new ::FunctionOwner(ptr);
        }

        event CommandCallback^ OnCallback;
    }
}

ポイント

  • ネイティブ側の関数ポインタ呼び出し規約を__stdcallにする
  • CLI側のデリゲートを、イベント登録用と関数ポインタ変換用の2つに分けて保持する
  • イベント発火関数を関数ポインタ変換用デリゲートに登録して、関数ポインタを取得する

関数ポインタ変換用デリゲートはメンバにしなくても一見良さそうですが、得られるポインタはあくまでデリゲートに対するものなので、インスタンスを保持し続けないとGCに回収されて無効になってしまいます。なので内部メンバとして持たせておくことが必要です。

この実装により、CLIラッパーで提供するライブラリに対して、別アセンブリによる拡張をプラグイン的に追加できるようになります。是非お試しください。

15
12
0

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
15
12