ネイティブコードから C# のメソッドをコールバック
2012/05/14 koga
今回は、ネイティブコード(アンマネージコード)から C# のメソッド、つまりマネージドコードをコールバックする方法について紹介します。
■C# のコールバック実装
ネイティブコードから C# のメソッドをコールバックさせる場合の手順を説明する前に、C# でコールバック関数を実装する方法について、簡単に説明します。C#/CLR の delegate についてご存じの方は、以下の説明は飛ばして、次の「ネイティブコードからコールバックさせる場合の手順」をご覧になって下さい。
C# でコールバック関数を実装する場合は、delegate を定義して、コールバック関数として用いるメソッドを、その delegate に割り当てます。delegate については、C# のプログラミングガイドの、次のページで説明されています。
delegate (C# Reference)
http://msdn.microsoft.com/en-us/library/900fyy8e%28v=VS.80%29.aspx
Delegates (C# Programming Guide)
http://msdn.microsoft.com/en-us/library/ms173171(v=vs.80).aspx
上のページで説明されているように、delegate は、メソッドに対する参照型(reference type)です。delegate は、delegate キーワードを使って、通常のメソッドと同じように宣言しますが、delegate と同じシグネチャを持つメソッドを、その実体として割り当てることができます。この時、宣言された delegate に対して、System.Delegate クラスの派生クラスがコンパイラによって生成され、インスタンス化されます。Delegate クラスのリファレンスは、次のページにあります。
Delegate Class
http://msdn.microsoft.com/en-us/library/system.delegate(v=vs.90).aspx
なお、delegate に割り当てることのできるメソッドは、static である必要はなく、インスタンスメソッドを割り当てることが可能です。たとえば、System.Threading の Thread クラスは、コンストラクタの引数に ThreadStart という delegate を受け取り、その delegate に割り当てられたメソッド実体を、インスタンス内部で生成するスレッドによって実行します。ここで、インスタンスメソッドを delegate に割り当てることができるので、C++ の場合とは違い、クラスのインスタンスは渡す必要がない、というわけです。
■ネイティブコードからコールバックさせる場合の手順
さて、本題です。ネイティブコードから C# のメソッドをコールバックさせる手順ですが、実は簡単です。必要な手順は、次の三つです:
- コールバックさせるメソッドに対応する delegate を定義する。
- 定義した delegate にメソッドを割り当てたのち、System.Runtime.InteropServices.GCHandle 構造体の Alloc() メソッドを適用して、delegate 実体に対する GCHandle インスタンスを生成する。
- delegate 実体に、System.Runtime.InteropServices.Marshal クラスの GetFunctionPointerForDelegate() を適用して、その戻り値を、関数ポインタとしてネイティブコードに渡す。
C のネイティブコード API で、コールバック関数の関数ポインタを受け取るものを使ってコールバックする場合ですと、関数ポインタの引数を IntPtr として P/Invoke の宣言を行い、System.Runtime.InteropServices.Marshal クラスの GetFunctionPointerForDelegate() の戻り値(IntPtr 型)を、関数ポインタの引数として渡せばよいのです。ただし、それだけですと、その関数ポインタに関連づけられた delegate の実体、つまり C# のメソッド(インスタンスメソッドの場合のインスタンスおよび、クラス)が、ガベージコレクション(GC)によって解放されてしまう可能性があります。そのため、メソッドを割り当てた delegate 実体に対して GCHandle のインスタンスを生成し、GC の対象外にする、というわけです。
GCHandle 構造体および、System.Runtime.InteropServices.Marshal クラスの GetFunctionPointerForDelegate() については、リファレンスの次のページをご覧ください。
GCHandle Structure
http://msdn.microsoft.com/en-us/library/khk3k17t(v=vs.90)
Marshal.GetFunctionPointerForDelegate Method
http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal.getfunctionpointerfordelegate(v=vs.90)
ここで述べた、ネイティブコード(アンマネージコード)からマネージドコードを呼び出す手順については、Bojan Resnik という人の Blog でも紹介されています:
Passing C++/CLI delegate to native code
http://resnikb.wordpress.com/2009/05/18/passing-ccli-delegate-to-native-code/
この Blog エントリでは、C# ではなく、C++/CLI の場合の例を説明していますが、C# の場合も同様です。上の説明と合わせ、参考にしてみて下さい。
■おまけ: .NET CF のガベージコレクション
上の説明で、ガベージコレクション(GC)について述べましたので、ついでに、.NET CF の GC について留意点を書いておきます。
まず、.NET CF の GC は、フル版の .NET Framework の GC とは実装が異なります。フル版の .NET Framework では(より正確には、フル版の .NET Framework の CLR では)、世代別 GC かつ、コンカレントまたはバックグラウンド GC が実装されています。詳しくは、次のページをご覧ください。
Fundamentals of Garbage Collection
http://msdn.microsoft.com/en-us/library/ee787088
一方、.NET CF では、コンパクション付きのマーク&スイープ GC です。これについては、.NET CF チームの Steven Pratschner という人が MSDN Blog に書いたエントリが参考になります:
An Overview of the .Net Compact Framework Garbage Collector
http://blogs.msdn.com/b/stevenpr/archive/2004/07/26/197254.aspx
The Design of the .Net Compact Framework CLR, Part III: GC Heap Management
http://blogs.msdn.com/b/stevenpr/archive/2005/12/14/503818.aspx
.NET CF の GC は、コンカレント GC ではなく、また、世代別 GC でもないため、GC が起きた際の、GC 実行中におけるマネージドコード全スレッドの停止と、停止期間について留意する必要があります。一般論としては、マネージドコードでは、リアルタイム処理に関わる動作を行わず、リアルタイム処理は全てアンマネージコードに任せるか、または、頻繁に GC が起きないように設計する方が良いと思います。GC が頻繁に起きないようにする方策としては、マネージドコードでも、バッファやオブジェクトのプールを実装し、過度に GC に頼らないようにすることが考えられます。もちろん、GC に頼らないということにこだわり過ぎて、全てのオブジェクトのメモリ管理を自前で行おうとして複雑な設計や実装をするのは、本末転倒です。とはいえ、数百KB以上のサイズのオブジェクトやバッファ領域を頻繁に使用する場合には、それらを都度 new で生成して GC に回収させるのではなく、あらかじめ必要な個数を生成しておき(プーリング)、それらを使いまわすプール方式で実装する方が良い場合は、少なくないと思います。
GC の場合も、チューニングを行う場合には、定性的な評価・予測に頼るのではなく、実測に基づく、定量的な評価・分析が重要です。たとえば、「GC.Collect() をアプリケーションが呼ぶのは間違いか?」という意味のタイトルの次の質問に対する回答でも、「それが有効な場合は、レアケースとして考えられる。ただし、十分に実測して、そのうえで効果を判定すべし。」と書かれています:
What’s so wrong about using GC.Collect()?
http://stackoverflow.com/questions/118633/whats-so-wrong-about-using-gc-collect
Entry Filed under: .NET Compact Framework, アプリケーション開発
2 Comments Add your own
1. Olli Warelius | 5月 26th, 2012 at 19:02
Just as good as your last post. Do you accept advertisers?
2. せろ | 5月 14th, 2013 at 01:21
いつもお役立ち情報をありがとうございます。
GetFunctionPointerForDelegate() なんですが、NETCF2.0 以降で必ずしも必要ないみたいです。(自動で呼ばれる?)
http://msdn.microsoft.com/ja-jp/library/vstudio/8bbftkst(v=vs.80).aspx
のPC用CallbackサンプルをCE用に変換しました。
public delegate int FPtr( int value );
public class LibWrap
{
// define ptr to delegate method
FPtr cb = new FPtr( App.DoSomething ) ;
// this wil fix the “cb” memory
GCHandle handle = GCHandle.Alloc(cb);
// not really needed?
IntPtr ptr = Marshal.GetFunctionPointerForDelegate(cb);
// call native
LibWrap.TestCallBack( cb, 99 );
// free handle
if (handle.IsAllocated)
handle.Free();
…
こんなコードを書いてみましたが、実は以下でも同様に動きます。
FPtr cb = new FPtr( App.DoSomething );
LibWrap.TestCallBack( cb, 99 );
callbackが複数の関数で有効でなければならないときは GCAllloc … Free がやっぱり必要みたいですが…
Leave a Comment
Some HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
Trackback this post | Subscribe to the comments via RSS Feed