fixedしたポインタにスコープを越えさせる

 皆さん大好き*1unsafeコード。
 滅多にない事ですが、P/Invoke を使ってなんかいろいろと変な事をしていると、こんなコードを書きたくなってくる事があります:

class Hoge
{
    private byte[] m_ptr;

    public unsafe byte* GetPointer()
    {
        // &(m_ptr[0])を取得したい!
    }
}

 今回は、こんな時にどうするか、というお話。

やっちゃいけないパターン

 まず第一に思いつくのが、こんなコードです:

public unsafe byte* GetPointer()
{
    fixed (byte* ptr = m_ptr)
    {
        return ptr;
    }
}

 もちろん、こんな危険なコードを書いてはいけません。
 MSDN には、次のように書かれています:

ステートメントのコードを実行すると、固定された変数の固定が解除され、ガベージ コレクションの対象になります。 そのため、fixedステートメントの外部にある変数へのポインターは指定しないでください。

 ……わからん! この糞機械翻訳めが!

 というわけで原文がこちらです:

After the code in the statement is executed, any pinned variables are unpinned and subject to garbage collection. Therefore, do not point to those variables outside the fixed statement.

 これが意味するところを正確に訳すのであればこうでしょうか? 太字機械翻訳では不正確になっている部分です。

ステートメント内のコードを実行し終えた後、固定された変数の固定は解除され、ガベージ コレクションの対象になります。そのため、fixedステートメント外部では、これらの変数をポインタを介して使用してはいけません。

 つまり、上記のコードでは、こんな事が起こるかもしれないわけです:

byte *ptr = hoge.GetPointer();
// ここで GC が走る(かもしれない)
SomeNativeFunction(ptr); // GC が走った場合、ptr は無効な値を指しているため大変な事になる

正しい方法

 勘のいい方なら、きっとこう考えるに違いありません。
「確かlock(obj) { ... }ステートメントは、

System.Threading.Monitor.Enter(obj);
try {
    ...
}
finally {
    System.Threading.Monitor.Leave(obj);
}

シンタックスシュガーだったはずだぞ。ならばfixedも同様に何らかのメソッドで実現できるに違いない」

 もしもfixedの開始と終了をそれぞれ別のメソッドに分解できれば、fixedしたポインタを関数の外に取り出せます。そのような機能はあるのでしょうか?

 はい、あります。
 少しポインタの使い方が変わってしまいますが、System.Runtime.InteropServices.GCHandleというクラスがその機能を提供しています。

 GCHandleを使ってポインタを取り出す方法がこちらです:

class Hoge
{
    private byte[] m_ptr;

    public GCHandle GetPointerHandle()
    {
        return GCHandle.Alloc(m_ptr, GCHandleType.Pinned);
    }
}

GCHandle handle = hoge.GetPointerHandle();
SomeNativeFunction((byte*)handle.AddrOfPinnedObject().ToPointer());
handle.Free();

 GCHandle.Alloc()GCHandleType.Pinnedを渡すと、アドレス固定モードのGCHandleが返ります。
 次に、得たGCHandleに対してAddrOfPinnedObject()を呼ぶと、その固定されたアドレスがIntPtrとして返ります。
 最後にIntPtrToPointer()を呼ぶと、IntPtrvoid*になってくれるので、これを目的の型(今回はbyte*)にキャストしてやります。
 ここまででfixedステートメントの開始部分。

 fixedステートメントの終了部分に相当するのは、handle.Free()です。これを呼ばないとアドレスが固定されたままになり、ガベージコレクションの効率が悪化するため、忘れずに呼ぶようにして下さい。

でも本当は

 まあ……こんな事をするくらいなら、大抵の場合は最初からこれでいいんですけどね:

class Hoge
{
    private byte[] m_ptr;

    public byte[] GetArray()
    {
        return m_ptr;
    }
}

fixed (byte* ptr = hoge.GetArray()) {
    SomeNativeFunction(ptr);
}

 ですが実際は、設計の都合上これではいけない事も大いにあります。
 例えば、上記のコードでは、このような事ができてしまうでしょう:

byte[] arr = hoge.GetArray();
arr[0] = 10; // ← arr が参照なので、hoge の中身書き換えられてしまう!

 これを防ぐためにはGetArray()m_ptrをコピーしてから返せばよいのですが、場合によっては配列サイズが巨大なのでコピーさせたくない場合もあるかもしれません。
 そんな時、(良い設計とは言えませんが)敢えてGCHandleを返す事で、「お前ら、特別な場合(つまり API に渡して中で弄って貰う時)以外はこの戻り値を弄るんじゃないぞ」という意図を明確にする事ができるわけです。

 もう少しまともな設計にするのであれば、GCHandleのラッパーを作ってこうするでしょう:

sealed unsafe class SafePointer<T> : IDisposable
{
    private readonly GCHandle m_handle;
    private IntPtr m_ptr = IntPtr.Zero;
    private bool m_disposed = false;

    public SafePointer( T[] ptr )
    {
        if ( null != ptr ) {
            m_handle = GCHandle.Alloc( ptr, GCHandleType.Pinned );
            m_ptr = m_handle.AddrOfPinnedObject();
        }
    }

    ~SafePointer()
    {
        Dispose();
    }

    public bool IsDisposed
    {
        get { return m_disposed; }
    }

    public static implicit operator IntPtr( SafePointer<T> ptr )
    {
        return ptr.m_ptr;
    }

    public void Dispose()
    {
        if ( m_disposed ) return;

        m_disposed = true;

        if ( m_handle.IsAllocated ) {
            m_handle.Free();
            m_ptr = IntPtr.Zero;
        }
    }
}

 void*を返すか、IntPtrを返すかはお好みで構いませんが、いずれにせよ似たような形になる事は確かなのではないかと思います。
 なおもちろんですが、このクラスは破棄されるまでオブジェクトをピン止めし続け、GCの効率を下げるので、ご利用は計画的に。

[完全版] 究極のC#プログラミング ~新スタイルによる実践的コーディング

[完全版] 究極のC#プログラミング ~新スタイルによる実践的コーディング

*1:大好きなのはいいけれど多用されちゃ困るんですが。