unmanagedクラスにmanagedなメンバを持たせる

以下のコードはビルドできません。「マネージ 'hoge' をアンマネージ 'Native' で宣言できません。」というメッセージが出ます。

class Native
{
public:
    StringBuilder^ hoge;
};

managedな変数hogeGCによって移動された場合に、unmanagedなクラスではその追跡ができなくなるのが要因です。これを克服する方法を以下に書いていきます。

なお、ここでいうmanagedな変数とは、ref classです。型名の後ろに ^ が付くものです。

方法1 : ポインタで保持する

GCで動かないよう固定するといえば、System::Runtime::InteropServices::GCHandleの出番です。

GCHandle::Alloc によってmanagedオブジェクトのハンドルを取得します。ハンドルはIntPtrに変換でき、またそのIntPtrからハンドルに戻すことができますから、unmanagedクラスではポインタを持っておくことにすればポインタ経由でmanagedなオブジェクトへの参照が確保できることになります。

using namespace System;
using namespace System::Runtime::InteropServices;
using namespace System::Text;

class Native1
{
public:
    void* hoge;
};

int main(array<System::String ^> ^args)
{
    Native1 n;
    StringBuilder^ sb = gcnew StringBuilder("Hoge");

    // managedオブジェクトをポインタとして持たせる
    n.hoge = (void*)(IntPtr)GCHandle::Alloc(sb);

    // ポインタからmanagedオブジェクトに変換
    {
        GCHandle handle = GCHandle::FromIntPtr(IntPtr(n.hoge));
        StringBuilder^ hoge = (StringBuilder^)handle.Target;
        Console::WriteLine(hoge->ToString());
    }

    return 0;
}

方法2 : GCHandleで保持する

方法1ではGCHandleをわざわざvoid*に変換して保持しましたが、実はunmanagedクラスでもCLIの「構造体」は持てるようです。ここでいう構造体とはC#でいうstruct, C++/CLIではvalue classのことです。GCHandleはvalue classですから、GCHandleを直接持たても良いのです。

using namespace System;
using namespace System::Runtime::InteropServices;
using namespace System::Text;

class Native2
{
public:
    GCHandle hoge;
};

int main(array<System::String ^> ^args)
{
    Native2 n;
    StringBuilder^ sb = gcnew StringBuilder("Hoge");

    // managedオブジェクトをGCHandleとして持たせる
    n.hoge = GCHandle::Alloc(sb);

    // GCHandleからmanagedオブジェクトに変換
    {
        StringBuilder^ hoge = (StringBuilder^)n.hoge.Target;
        Console::WriteLine(hoge->ToString());
    }

    return 0;
}

方法3: gcroot

上記2つの方法は面倒ですし、この調子でメンバ変数が増えていくと、クラスの定義を見てもハンドルだらけで型が全く分りません。しかし幸い便利なテンプレート構造体が用意されています。msclr::gcroot<T>です。GCHandleとの変換処理を覆い隠してくれます。

#include <msclr/gcroot.h>
using namespace System;
using namespace System::Runtime::InteropServices;
using namespace System::Text;

class Native3
{
public:
    msclr::gcroot<StringBuilder^> hoge;
};

int main(array<System::String ^> ^args)
{
    Native3 n;
    StringBuilder^ sb = gcnew StringBuilder("Hoge");

    // 代入
    n.hoge = sb;

    // 参照
    StringBuilder^ hoge = n.hoge;
    Console::WriteLine(hoge->ToString());

    return 0;
}

このように、まったく普通の変数と同じ感覚でアクセスすることができます。gcroot内部では、方法1と同じ方法でポインタによりハンドルを保持する仕組みになっています。


なお、
#include <msclr/gcroot.h> を使うと msclr::gcroot<T> になりますが、
#include <gcroot.h> も存在し、こちらは名前空間が無く gcroot<T> で使うことができます。



ということで実用上はgcroot一択です。しかしながら、どのような仕組みかを知る上で方法1, 2は価値のあるものです。