参照渡しの罠

ラッパーを書いていると当然のようにポインタが山のように出てきます。中でもcvCreateTrackbarは鬼門の一つです。定義は次のようになっています。

CV_EXTERN_C_FUNCPTR( void (*CvTrackbarCallback)(int pos) );

int cvCreateTrackbar( const char* trackbar_name, const char* window_name,
                      int* value, int count, CvTrackbarCallback on_change );

ここで厄介なのはvalueとon_changeです。valueはトラックバーの初期位置です。ポインタで渡しているので、トラックバーの操作に従い現在の目盛りの位置が入ります。on_changeにはトラックバーが操作されたときのコールバック関数を関数ポインタで渡します。


これを順当にC++/CLIでラップすると、こんな感じになります(文字列ポインタの解放とかは割愛)。

// コールバック関数として渡すデリゲート
[UnmanagedFunctionPointer(CallingConvention::Cdecl)]
public delegate void CvTrackbarCallbackHandler(Int32 pos); 

// cvCreateTrackbarのラッパー
Int32 CV::CreateTrackbar(String^ trackbar_name, String^ window_name, [In][Out] Int32% value, Int32 count, CvTrackbarCallbackHandler^ on_change)
{
	char* namet = Marshal::StringToHGlobalAnsi(trackbar_name);
	char* namew = Marshal::StringToHGlobalAnsi(window_name);		
	pin_ptr<int> value_pin = &value;
	CvTrackbarCallback callbackPtr = NULL;
	if(on_change != nullptr){
		callbackPtr = static_cast<CvTrackbarCallback>(Marshal::GetFunctionPointerForDelegate(on_change).ToPointer());
	}
	return ::cvCreateTrackbar(namet, namew, value_pin, max, callbackPtr);
}

これをC#からはこういう感じで使うわけです。

int value = 100;
CvTrackbarCallbackHandler callback = delegate(int pos){
	Console.WriteLine(value);
};
CV.CreateTrackbar("hoge", "window1", ref value, 256, callback);

ところがこれに問題があって、このコールバック関数の処理でメモリを食いまくったりなどしてGCが発生すると、変数valueやcallbackのアドレスが変わってしまうようなのです。しかしアドレスが変わってもネイティブ側には追跡参照はしてくれませんから、そこからはトラックバーを動かしても何も起こらない、ということになってしまいます。



これの解決策は実のところ結局よくわからないままなのですが、GCHandle.Allocをするとcallbackの方は解決します。valueの方は若干延命するようですがそのうち同じ症状になります。あとは変数をインスタンス変数で持たせたりしてスコープが終わってGC、ということにならないようにしたり、などなど。

Int32 CV::CreateTrackbar(String^ trackbar_name, String^ window_name, [In][Out] Int32% value, Int32 count, CvTrackbarCallbackHandler^ on_change)
{
	char* namet = Marshal::StringToHGlobalAnsi(trackbar_name);
	char* namew = Marshal::StringToHGlobalAnsi(window_name);		
	pin_ptr<int> value_pin = &value;
	this->gch_value = GCHandle::Alloc(value);
	CvTrackbarCallback callbackPtr = NULL;
	if(on_change != nullptr){
		this->gch_callback = GCHandle::Alloc(on_change);
		callbackPtr = static_cast<CvTrackbarCallback>(Marshal::GetFunctionPointerForDelegate(on_change).ToPointer());
	}
	return ::cvCreateTrackbar(namet, namew, value_pin, max, callbackPtr);
}

しかしそれでもvalueは解決しなかったので、結局ref渡しはやめにしました...


.NETのクラスライブラリ設計 開発チーム直伝の設計原則、コーディング標準、パターン (Microsoft.net Development Series)

.NETのクラスライブラリ設計 開発チーム直伝の設計原則、コーディング標準、パターン (Microsoft.net Development Series)