やったこと
Winmm.dllのWAVE関連関数をマネージコードから呼び出し
waveInOpen()でwaveInProcのデリゲートの関数ポインタをコールバックとして登録
WAVEHDR構造体をwaveInAddBuffer()に参照渡し
現象
waveInProcコールバックの呼び出しで例外(GCされたデリゲートの呼び出し)
コールバック処理内でAccess Vaiolation
デリゲートがGCされて例外
関数ポインタを取得する際に匿名インスタンスを使用したことが原因であった。
メソッド本体は自身のインスタンスなのでGCされない
関数ポインタはインスタンスメソッドを指している
という2つの思い込みがあったため、ハマった。
実際にはネイティブからのコールバックで参照されるのはあくまでデリゲートであって関数ポインタは媒介にすぎない。
よってデリゲート自身のインスタンスが存在しなければならない
というわけでメンバにデリゲートを追加して解決
WAVEHDR構造体がGCされて例外
コールバック内でWAVEHDR構造体の情報(バッファサイズ、バッファへのポインタ等)を利用していた
WAVEHDR構造体はコールバック関数の引数としてポインタ(IntPtr)として渡されるため、コールバック内ではポインタ経由で構造体メンバにアクセスしていた
ここで、コールバックメソッド(上記デリゲート)の処理中にGCが走りWAVEHDR構造体が最適化によって移動させられても、ポインタの値は元の構造体のアドレスを指し続ける。
このため、GC後はメモリ上のおかしな場所にアクセスすることになり、結果として例外を吐いて終了
マネージコードでポインタを扱う場合、オブジェクトを固定してから操作するのが大原則であるが今回はそもそもコールバックの引数として与えられたポインタであったためこの点を見逃していた。
というわけで、GCHandle.Alloc(obj, GCHandleType.Pinned)~GCHandle.Free()で囲って例外が発生することはなくなった。
教訓
- ポインタはあくまでポインタ
- 実際にアクセスされるものは何なのか考える
- マネージコードでポインタ使うならちゃんとピンどめ(固定)しましょう
蛇足
懸念が残るのは、そもそも構造体をwaveInAddBuffer() APIに渡しているし、コールバックで渡ってくる時点でポインタ渡しになっている。P/Invokeの内部は詳しくないので分からないがマネージ<ー>ネイティブ間やネイティブに渡ってからのオブジェクトの扱いはどうなっているのだろうか。
見た限りではオブジェクトの移動等は発生していないようなのでP/Invokeの仕組みとしてGCの影響等を受けないようにしているのだと思うが、中身が見えないのでちょっと気持ち悪い。
このキャプチャを見ると、WAVEHDR構造体がごっそり移動していること、ポインタpのアドレスは変わっていないこと、pの内容はおかしな内容になっていることがわかる。
(と言うかここまで見てやっと気づいた)