c# ネイティブリソースをラップする方法 (その1)

その1では、ネイティブリソースをイミュータブルなクラスでラップする方法を示します。イミュータブルとは、インスタンスの内部状態が、変化しないクラスのことです。ナイーブな実装は↓のようなものです。

class A
{
  protected IntPtr handle;

  public A()
  {
    handle = Api.Alloc();
  }

  ~A()
  {
    Api.Release(handle);
  }
  
  public void Method()
  {
    Api.Method(handle);
  }
}

Api クラスでネイティブ API の呼びだしが実装されていることを想定した疑似コードです。実際には、Api.Alloc では、ILCreateFromPathW でアイテムIDリストを作成したり、Api.Release では、ILFree で開放したりします。

しかし、c# では、ファイナライザーの実行タイミング がかなりアグレッシブなため、(環境によっては)、稀に不正なメモリーアクセスなどの原因で、クラッシュします。 Api.Method(handle) 実行中に、~A() が呼ばれる場合があるためです。これを避けるには、↓のようにします。

class A
{
  protected IntPtr handle;
  protected int pinCount = 0;

  public A()
  {
    handle = Api.Alloc();
  }

  ~A()
  {
    Debug.Assert(pinCount == 0);
    Api.Release(handle);
  }
  
  public void Method()
  {
    Pin();
    try
    {
      Api.Method(handle);
    }
    finally
    {
      Unpin();
    }
  }

  protected void Pin()
  {
    Debug.Assert(pinCount >= 0);
    ++ pinCount;
  }

  protected void Unpin()
  {
    Debug.Assert(pinCount >= 1);
    -- pinCount;
  }
}

handle を使う場合には、Pin()Unpin() を呼びだしてファイナライザーが呼び出されないようにしています。 handle はスレッド間で共有されるので一般的には、ロックが必要ですが、handle をコンストラクターとファイナライザー以外で変更しなければロックは不要だと思います。(イミュータブルなクラスを採用したのはこのためです)

handle を外部で使用しない方が安全ですが、公開する場合には、Pin()Unpin()public で公開して、handle を保護する必要があります。

本質的には、pinCount や、 Pin() は不要ですが、コンパイラの最適化によって、Unpin() が消えてしまうの懸念があるのと、デバッグの助けになるので入れた方がより安全だと思います。

pinCount があるので、厳密にはイミュータブルでないような気もしますが、外から見たらイミュータブルに見えるので、イミュータブルと呼んでよいのではないでしょうか?自分は残念ながらイミュータブルの名付け親じゃないのでわかりません。

Pin() を使用する方法の他、GC.KeepAlive を使用する方法 もあるようです。記事を書いた後でネイティブリソースで検索したら、CA2115: ネイティブ リソースを使用しているときには GC.KeepAlive を呼び出します がでてきたので発覚しました。こちらの方法の方が良いかもしれませんね。

また、ミュータブルなクラスでは、lock の必要があるので、ネイティブリソースをラップする方法 (その2) のように保護すると良いでしょう。

となりのページ

このサイトについて

このサイトのページへのリンクは自由に行っていただいてかまいません。
このサイトで公開している全ての画像、プログラム、文書の無断転載を禁止します。

連絡先

ここをクリック すると表示されるページから作者へメールで連絡できます。

共有