ラッパーのメモリ管理

C#C/C++のライブラリのラッパークラスを作るときの基本形は、こんな感じになると思います。

class HogeWrapper : IDisposable
{
    IntPtr ptr;

    public HogeWrapper()
    {
        ptr = createHoge();
    }
    public void Dispose()
    {
        releaseHoge(ptr);
    }

    [DllImport("hoge.dll")]
    static extern IntPtr createHoge();
    [DllImport("hoge.dll")]
    static extern void releaseHoge(IntPtr ptr);
}

このほかにメソッド・プロパティを実装する際も、同様に内部ではptrを通してネイティブの関数を呼んであげるようにします。


しかしここで、実はcreateHogeの定義がこんなことになっていたとしましょう。(適当に書いたから間違ってるかも)

typedef struct Hoge
{
    char too_long_array[1024*1024];
}
Hoge;

extern "C" __declspec(dllexport) Hoge* __cdecl createHoge()
{
    return new Hoge;
}

Hogeのメンバであるtoo_long_arrayが1024*1024バイトで、すなわちこの構造体は単純計算で1MBもメモリを食っています。しかし.NET Framework側ではそんなことは一切関知しないようです。.NET側としては、大雑把に言えばcreateHogeからの返り値であるIntPtrの分の4バイト(x64なら8バイト?)だけが消費されたと認識されます。

この調子でcreateHogeを何度も呼び出していれば、そのうちメモリが逼迫してきます。通常ならここでGCの出番なのですが、.NETとしては、例え1000回呼び出されてメモリが1GBぐらい食われていようと、「IntPtrが1000個でたかが4000バイト、まだまだいける」ということになってしまうようです。


そこで、GC.AddMemoryPressureを使います。
http://msdn.microsoft.com/ja-jp/library/system.gc.addmemorypressure(VS.80).aspx
このメソッドを使うと、.NET Frameworkのランタイムが関知しないところでどれだけメモリが食われたのかを教えてあげることができます。当然の前提としてsizeof(Hoge)がわかっている必要がありますので、あらかじめヘッダを見るなりして調べておきます。

対になるGC.RemoveMemoryPressureメソッドと組み合わせて、このように使うといいのではないかと。

class HogeWrapper : IDisposable
{
    IntPtr ptr;
    const int SIZE_OF_HOGE = 1024*1024;

    public HogeWrapper()
    {
        ptr = createHoge();
        GC.AddMemoryPressure(SIZE_OF_HOGE);
    }
    public void Dispose()
    {
        releaseHoge(ptr);
        GC.RemoveMemoryPressure(SIZE_OF_HOGE);
    }

    [DllImport("hoge.dll")]
    static extern IntPtr createHoge();
    [DllImport("hoge.dll")]
    static extern void releaseHoge(IntPtr ptr);
}