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
として返ります。
最後にIntPtr
にToPointer()
を呼ぶと、IntPtr
がvoid*
になってくれるので、これを目的の型(今回は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の効率を下げるので、ご利用は計画的に。
*1:大好きなのはいいけれど多用されちゃ困るんですが。