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

# 概要

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

# ネイティブ側の準備

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

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

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

```cpp
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でラッパーを作る場合は、上記のクラスなら次のようにすると思います。(デストラクタやファイナライザは別途いいようにしておくとして)

```cpp
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というイベントを作ることにします。

```cpp
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つ使うのがポイントです。

```cpp
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しておくと良いでしょう。

```cpp
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ラッパーで提供するライブラリに対して、別アセンブリによる拡張をプラグイン的に追加できるようになります。是非お試しください。