C++→C(DLL)→C#(クラスライブラリ)

C++のアプリから伝統的なC言語DLL(__declspec(dllexport))を呼び出したら実はそいつがC++/CLIで作られていて、そこからC#で作ったクラスライブラリを呼ぶぜ!みたいなパターン。
わかりやすく言えば、SusieプラグインC#作りたい、とかそんな話だよ!わかりやすくないか!!
興味のない人のためにここから先は「続きを読む」で。長文注意。
とりあえずVC++にソリューションを一個作って、そのなかにDLLとEXEのプロジェクトを作っていく。ちなみにここではVC++(とVC#)2008のExpressEditionを使用しています。
まず、伝統的なDLLの作成。VC++でWin32アプリケーションのテンプレートを使って出力をDLLにすれば何もしなくても出来上がる奴。エクスポートする関数にはごにょごにょと修飾子をつける必要があって、まぁこんな感じだ。

//c.dll
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

#ifdef __cplusplus
#define PluginAPI extern "C" __declspec(dllexport)
#else
#define PluginAPI __declspec(dllexport)
#endif

PluginAPI void Initialize(void)
{
    //エクスポートする"Initialize"関数の中身
}

PluginAPI void Terminate(void)
{
    //エクスポートする"Terminate"関数の中身
}

次に、このDLLを呼び出すプロジェクトを作る。Win32のコンソールだろうがウィンドウだろうが何でもいいので用意する。まぁ、普通はこれがすでにあって、こいつが使うライブラリをC#で作りたいっていう状況なんだけど。そうじゃなきゃ全部C#で書けって話ですよ。

//sample.exe
#include <windows.h>
typedef void pluginApiPoc(void);

int APIENTRY _tWinMain(HINSTANCE hInstance,
                       HINSTANCE hPrevInstance,
                       LPTSTR    lpCmdLine,
                       int       nCmdShow)
{
    HMODULE module = ::LoadLibrary(TEXT("c.dll"));
    pluginApiPoc* init = ::GetProcAddress(module, "Initialize");
    pluginApiPoc* term = ::GetProcAddress(module, "Terminate");
    init();
    term();
    ::FreeLibrary(module);
}

DLLをC++のアプリから動的ロードで呼び出すのは、大体こんな感じに違いない。
この状態では、きわめて普通に「C++で書かれたアプリとDLLがある」状態なので、これを目標にしている状態に近づけていく。
呼び出されるものがなければ仕方がないので、クラスライブラリをC#(VB.NETでもいいけどね)で書く。VC#を起動して、「クラスライブラリ」を作ってビルドすればいいわけだよ。うん。何も難しくない。そこに機能を実装して、ビルドしたらdllができるのです。これでSampleLibrary.SampleClassをエクスポート(public宣言)するSampleLibrary.dllができたとしようか。
次に、VC++に戻ってDLLをC++からManaged C++、もといC++/CLIに変換する。とはいえ現時点ではDLLに中身が何もないのでやることは簡単。
プロジェクトのプロパティからすべての構成の「構成プロパティ>全般>共通言語ランタイムサポート」を「共通言語ランタイムサポート(/clr)」にするだけ。すると「共通プロパティ」のところでフレームワークが選択できるようになるので、ここはデフォルトのまま.NET Framework3.5にでもしておく(別に何でもいいですが・・・)。それができたらビルドして実行。ぶっちゃけ何も変わらない。本当にCLIになったのかどうかさえ見た目ではわからない。変わったことといえば、いざ実行してみるとDLLをロードしてもロードしたことにならなくてデバッグできなくなっちゃったことなんだけど・・・うわぁ、困る!!まぁ、そこは仕方ないから諦めよう。
・・・話を戻そうか。
DLLがC関数のインターフェースを備えたC++/CLIのDLLになった状態なので、ここからC#のクラスを使う。Initializeでインスタンスを作って、Terminateで開放。そんな感じ。途中の関数は必要に応じて順次定義すればいい。
まずは参照設定。DLLのプロジェクトプロパティにある「共通プロパティ」で「新しい参照の追加」をする。追加するのは、さっき作った.NETのライブラリなのだけど、レジストリに登録されているわけではないので普通に「参照」からDLLを選択してインポートする。SampleLibrary.dllを選べばよい。SampleLibrary.dllに依存関係があれば(FormsとかDrawingとかね)、それも一緒に参照しておくと良いでしょう。
参照設定が終わったら、とりあえずクラスライブラリのクラスをインスタンス化してみましょう。C++/CLIでは、こんな感じ。

//c.dll
using namespace SampleLibrary;

PluginAPI void Initialize(void)
{
    SampleClass^ sample = gcnew SampleClass();
}

SampleLibraryというのは、C#で作ったクラスライブラリのネームスペース。その中に、SampleClassというクラスが定義されております。マネージ型なので"^"で修飾して、newの代わりにgcnewを使う。
基本的にはこれだけやればC#のライブラリを呼べたことになってると思うんだけど、ここで一つ注意点が。それは、C++/CLIではグローバル変数としてマネージドなオブジェクトを配置できない、ということ。たとえば、上記コードの宣言部分を関数の外に出すとコンパイルエラーになります。何でかって聞かれても困るんだけどC#でもできないだろうが!!そういうもんだよ!!
さて、この対策としては、グローバルに直接置けないだけなので(マネージド)クラスの静的メンバーとしておいてやればいい。

//c.dll
ref class GlobalInstance
{   //グローバルなマネージドオブジェクトの入れ物
public:
    static SampleClass^ sample;         //クラスライブラリのインスタンス保存用
};

PluginAPI void Initialize(void)
{
    SampleClass^ sample = gcnew SampleClass();
    GlobalInstance::sample = sample;    //インスタンスを保存
}

PluginAPI void Terminate(void)
{
    GlobalInstance::sample = nullptr;   //インスタンスを開放
}

はい、これで完成ですね。c.dllはマネージドでないアプリ(sample.exe)のためのマネージドクラスライブラリ(SampleLibrary.dll)のラッパーになりました。案外簡単ですね。お疲れ様でした。