はじめに
C++(≠C++/CLI)からC#のメソッド1を呼び出すには次の方法があります。
- COMを使用する
 すべての.NETアセンブリはCOMコンポーネント2なので簡単にできそうですが、ジェネリクスの扱いに難がありそうな予感がします。今回は調べていません。
- C++/CLIでラッパーを作成する
 今回調べた方法です。
.NET Core用のC++/CLIが無いので対象は.NET Frameworkです。
マネージド/アンマネージド
C++/CLIではマネージド/アンマネージドの区別が重要になります。
マネージド/アンマネージドコード
- マネージドコード(デフォルト)
- ILにコンパイルされます。
- .NETから呼び出せます。
- エクスポート(dllexport)できません。従って普通のC++から呼び出せません。
 
- アンマネージドコード
- 機械語にコンパイルされます。
- エクスポート(dllexport)でき、普通のC++から呼び出せます。
 
C++/CLIで作成するDLLにはマネージドコードとアンマネージドコードを共存させることができます。
それぞれ#pragma managedと#pragma unmanagedで切り替えられます。
マネージド/アンマネージドクラス
- マネージドクラス
- .NETの参照型に対応します。
- マネージドコードで定義する必要があります。
- マネージドクラスを集約できます(メンバー変数に持てます)。
- GCの対象になります。
 
- アンマネージドクラス
- .NETの値型に対応します。
- マネージドクラスを集約できません。
- GCの対象になりません。
 
ハンドル
ハンドルはマネージドクラス専用のポインターのようなもので、T* ではなくT^という型名を持ちます。
// アンマネージドクラス
T* v = new T()
// マネージドクラス
T^ v = gcnew T()
アンマネージドクラスはハンドルを直接メンバー変数に持つことができません。
ただしgcrootというクラステンプレートでラップすることでメンバー変数にできます。
戦略
C++/CLIの最もよくある使われ方は、C++のライブラリをC#(.NET)で使えるようにするために使う事でしょう。この時は、C++クラスのラッパーをマネージドクラスで作成することになります。
逆はもうひと手間掛かります。
- C++から呼び出すためには、アンマネージドコードで定義したアンマネージドクラスでなければなりません。
- .NETのメソッドを呼び出すためには、マネージドコードでなければなりません。
これらの制限を超えるため、2つのラッパーを作成します。
- マネージドコードで定義したアンマネージドクラス
 .NETのクラスをメンバーに持ち、メソッドを呼び出します。
- アンマネージドコードで定義したアンマネージドクラス
 1.のラッパーでありC++にエクスポートするクラスです。
2.はC++側に見せるため、1.の実装に依存できません。2.を基本クラスとし、1の実装は隠蔽します3。
実装
簡単のため、以下のシンプルなC#のクラスを作成します。これをC++から呼び出します。
using System;
namespace ManagedLibrary
{
    public class ManagedClass
    {
        public void SayHello()
        {
            Console.WriteLine("Hello, C#");
        }
    }
}
ラッパーを作成する
まずC++にエクスポートするクラスを作成します。ヘッダーファイルがC++とC++/CLIの両方でコンパイルできるように注意します。
# pragma once
# pragma unmanaged
# include <memory>
# ifdef WRAPPER
# define EXPORT __declspec(dllexport)
# else
# define EXPORT __declspec(dllimport)
# endif
namespace Wrapper
{
    class Wrapper {
    public:
        virtual void SayHello() = 0;
        virtual ~Wrapper() = default;
    private:
        Wrapper(const Wrapper&) = delete;
        Wrapper(Wrapper&&) = delete;
        Wrapper& operator=(const Wrapper&) = delete;
        Wrapper& operator=(Wrapper&&) = delete;
    };
    EXPORT extern std::unique_ptr<Wrapper> CreateWrapper();
}
ファクトリーメソッドも宣言しました。生のnewは邪悪ですからunique_ptrを使用します。
次にC#のメソッドを呼び出すクラスを作成します。
# include "wrapper.h"
# pragma managed
# include <gcroot.h>
namespace Wrapper {
    public class WrapperImpl : public Wrapper
    {
    public:
        WrapperImpl()
        {
            managedObject = gcnew ManagedClass();
        }
        void SayHello() override
        {
            managedObject->SayHello();
        }
    private:
        gcroot<ManagedClass^> managedObject;
    };
    std::unique_ptr<Wrapper> CreateWrapper()
    {
        return std::make_unique<WrapperImpl>();
    }
}
ラッパーを利用する
最後に次の簡単なアプリケーションでラッパーを利用してみましょう。C++/CLIではなくC++でコンパイルします。
# include "wrapper.h"
int main()
{
    Wrapper::CreateWrapper()->SayHello();
}
実行結果
Hello, C#
参考サイト
余談
WrapperImpl::managedObjectはクラス内メンバーイニシャライザで初期化したいところですが、デフォルトコンストラクタを{}ではなく=defaultで実装したところ、アンマネージドコードでgcnewは使用できない(C3821)と言われてしまいました。ハマりそうなので普通にコンストラクタ内で初期化しています。