- PR -

C# SafeHandleの派生クラスの使い方

1
投稿者投稿内容
ひろし
ぬし
会議室デビュー日: 2002/09/16
投稿数: 390
お住まい・勤務地: 兵庫県
投稿日時: 2008-02-11 23:14
【質問】
下記のようなソースコードについても、共有バッファ内部のIntPtrをSafeHandleの派生クラスへ置き換えることで予期しないトラブルによるメモリリークを防止する効果をより高めることができるのでしょうか?
また、SafeHandleの派生クラスの使い方がよく理解できず、IntPtrをカプセル化する手順が分かりません。

【背景】
下記のソースコードをコード分析にかけたところ、下記の提案を受けたのが質問のきっかけです。アンマネージドメモリのメモリリークは可能な限り避けたいので、本当に効果があるなら、きちんと対処したいと考えました。
例えば、現在のコードではMarshal.FreeHGlobal()自体が失敗するとどうしようもありませんが、そのような場合でもうまく対処してくれるのでしょうか?
そもそも、SafeHandleの派生クラスの動作メカニズムが理解できていないため、どこがどうIntPtrより優れているのかが理解できません。

CA2006 : Microsoft.Reliability :
SafeHandle または CriticalHandle で置き換えるべきかどうかを決定するために、'共有バッファ._先頭番地' ('IntPtr' インスタンス) の使用法を確認してください。
上記エラーのヘルプ(参考)
http://msdn2.microsoft.com/ja-jp/library/ms182294.aspx

SafeHandleクラス(参考)
http://msdn2.microsoft.com/ja-jp/library/system.runtime.interopservices.safehandle(VS.80).aspx

SafeHandleZeroOrMinusOneIsInvalid クラス(参考)
http://msdn2.microsoft.com/ja-jp/library/microsoft.win32.safehandles.safehandlezeroorminusoneisinvalid.aspx

SafeHandleMinusOneIsInvalid クラス(参考)
http://msdn2.microsoft.com/ja-jp/library/microsoft.win32.safehandles.safehandleminusoneisinvalid.aspx

// サンプルソースコード

// アンマネージドメモリの先頭番地(IntPtr)をデバイスドライバーに渡します。
// デバイスのDMAがアンマネージドメモリに直接データを書き込みます。
// 共有バッファクラスはデバイスドライバーに書き込み先(=共有バッファ)の管理、
// つまり、(1)領域の確保(2)領域の解放(3)DMAが書き込んだデータの読み出し、
// を行います。


using System;
using System.Runtime.InteropServices;

namespace test
{
/// <summary>
/// アンマネージドメモリに共有バッファを作成します。
/// </summary>
public sealed class 共有バッファ : IDisposable
{
/// <summary>
/// 共有バッファを作成します。
/// </summary>
/// <param name="バッファ件数"></param>
public 共有バッファ(int バッファ件数)
{

if (バッファ件数 > 0)
{
_先頭番地 = Marshal.AllocHGlobal(バッファ件数 * _データ長);
}
else
{
_先頭番地 = IntPtr.Zero;
}

if (_先頭番地 != IntPtr.Zero)
{
_バッファ件数 = バッファ件数;
}
else
{
_バッファ件数 = 0;
}
}

~共有バッファ()
{
lock (this)
{
リソースの解放();
}
}

#region IDisposable メンバ

public void Dispose()
{
lock (this)
{
リソースの解放();
GC.SuppressFinalize(this);
}
}

#endregion

private void リソースの解放()
{
if (_先頭番地 != IntPtr.Zero)
{
Marshal.FreeHGlobal(_先頭番地);
_先頭番地 = IntPtr.Zero;
}
}

private readonly int _バッファ件数;

public int バッファ件数
{
get { return _バッファ件数; }
}

private IntPtr _先頭番地;

/// <summary>
/// この先頭番地を経由して直接アンマネージドメモリに書き込みます。
/// </summary>
public IntPtr 先頭番地
{
get
{
return _先頭番地;
}
}

private readonly int _データ長 = sizeof(int);

public int データ長
{
get { return _データ長; }
}

/// <summary>
/// アンマネージドメモリに書き込まれたデータを配列として読み出します。
/// </summary>
/// <param name="データの終端位置">バッファ内での最新データの格納位置[件数]を指定します。</param>
/// <param name="取得件数">取得するデータの件数を指定します。</param>
/// <returns>アンマネージドメモリからコピーしたデータを返します。</returns>
public int[] データの取得(int データの終端位置, int 取得件数)
{
int 取得可能件数 = Math.Min(取得件数, _バッファ件数);
int[] ret = new int[取得可能件数];
if (データの終端位置 >= 取得可能件数)
{
// コピーするデータがバッファの境界を越えないため、コピー操作が1回で済みます。
IntPtr コピーの開始番地 = (IntPtr)(_先頭番地.ToInt32() + (データの終端位置 - 取得可能件数) * _データ長);
Marshal.Copy(コピーの開始番地, ret, 0, 取得可能件数);
}
else
{
// コピーするデータがバッファの境界を越えるため、コピー操作を2回に分けます。

// コピーするデータの最初の部分
int 最初にコピーする件数 = 取得可能件数 - データの終端位置;
IntPtr 最初にコピーする開始番地 = (IntPtr)(_先頭番地.ToInt32() + (_バッファ件数 - 最初にコピーする件数) * _データ長);
Marshal.Copy(最初にコピーする開始番地, ret, 0, 最初にコピーする件数);

// コピーするデータの残りの部分
Marshal.Copy(_先頭番地, ret, 最初にコピーする件数, 取得可能件数 - 最初にコピーする件数);
}
return ret;
}
}
}
くまっち
大ベテラン
会議室デビュー日: 2008/01/18
投稿数: 169
お住まい・勤務地: 茨城県のどこか。
投稿日時: 2008-02-12 18:28
コードの信頼性は高まります。
より確実に開放処理が実行されるようです。(非同期例外発生時とか)

簡単な使い方は・・・下記のような感じでしょうか。
コード:
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public sealed class SafeGlobalAllocateHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    // 既定クラスの抽象メソッドを実装
    protected override bool ReleaseHandle()
    {
        System.Runtime.InteropServices.Marshal.FreeHGlobal(this.handle);
        return true;
    }

    // コンストラクタの定義
    public SafeGlobalAllocateHandle(int size) : base(true)
    {
        this.SetHandle(System.Runtime.InteropServices.Marshal.AllocHGlobal(size));
    }

    // ToDo:他拡張したい機能を以下に実装します。
}


下記記事で詳しく述べられています。
http://msdn.microsoft.com/msdnmag/issues/05/10/Reliability/default.aspx?loc=jp

参考になれば幸いです。
ひろし
ぬし
会議室デビュー日: 2002/09/16
投稿数: 390
お住まい・勤務地: 兵庫県
投稿日時: 2008-02-15 12:55
ご回答ありがとうございます。

参考資料を見ながら試しています。
yayadon
常連さん
会議室デビュー日: 2003/07/23
投稿数: 41
投稿日時: 2008-02-20 04:13
上の 共有バッファ クラスですが,
ファイナライザを設定しているので 開放 は(たぶん)OKなんですが,

_先頭番地 = Marshal.AllocHGlobal(バッファ件数 * _データ長);

のところで,

 _先頭番地

変数に代入する瞬間に 非同期例外が起きると,アウトになります。

P/Invoke の宣言時に,IntPtr と宣言する箇所を
SafeHandle系のクラスに換えることで,
ハンドルの代入を相互運用サービスが保障してくれます。
インスタンスを作成して,そのプライベートな handle に設定しておいてくれます。

今回の例だと,P/Invoke でないので,
AllocHGlobal(int size) を作ってやって,

コード:
    //[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]

[SecurityPermission(SecurityAction.Demand, UnmanagedCode = false)]
internal sealed class SafeAllocHGlobalAllocHandle : SafeHandle
{
private SafeAllocHGlobalAllocHandle() : base(IntPtr.Zero, true) { }

public static SafeAllocHGlobalAllocHandle AllocHGlobal(int size)
{
//先に作成 コンストラクトを成功するため
SafeAllocHGlobalAllocHandle tmp = new SafeAllocHGlobalAllocHandle();

//その後に割り込み不可にする(atomicityの保障)
RuntimeHelpers.PrepareConstrainedRegions();
try { }
finally
{
tmp.handle = Marshal.AllocHGlobal(size);
}

return tmp;
}

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
Marshal.FreeHGlobal(handle);
return true;

//if (IntPtr.Zero == GlobalFree(handle))
//{
// return true;
//}
//else
//{
// return false;
//}
}

public override bool IsInvalid
{
get { return (IntPtr.Zero == handle); }
}

//[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
//[DllImport("kernel32.dll", SetLastError = true)]
//public static extern SafeAllocHGlobalAllocHandle GlobalAlloc(uint uFlags, IntPtr dwBytes);

//[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
//[DllImport("kernel32.dll", SetLastError = true)]
//private static extern IntPtr GlobalFree(IntPtr hHeap);

}



で,

SafeAllocHGlobalAllocHandle _globalHandle;
_globalHandle = SafeAllocHGlobalAllocHandle.AllocHGlobal(...);

です。

ふつうは,P/Invoke になるので,

SafeAllocHGlobalAllocHandle _globalHandle;
_globalHandle = SafeAllocHGlobalAllocHandle.GlobalAlloc(...); // (A)

(A) のところで,フレームワーク側が,
SafeAllocHGlobalAllocHandleインスタンスを作成して
そのプライベートな handle にセットしてくれた後,
インスタンスを _globalHandle にセットしてくれます。
atomicity を保障してくりれます。

また,
コンストラクタがプライベートでも作成してくれるので,
プライベートにしておくのがベストでしょう。たぶん。

ReleaseHandle() は,フレームワーク側が呼んでくれるものです。
それは保障されてます。

明示的に閉じる場合は,Close() や Dispose() を呼びます。
_globalHandle.Close();

クラスのメンバで

IntPtr _globalHandle;

としているところを

SafeAllocHGlobalAllocHandle _globalHandle

とする感じです。
このメンバ自体で最終的な破棄を保障するので,
これをメンバに持つクラスは,Dispose()パターンは必ずしも必要ありません。
終了のためのメソッドを外に公開するのなら Close() だけの公開も可能です。

public void Close()
{
_globalHandle.Close();
}



// 結構,あやしげな?やり方もWEB上で見られます。

[ メッセージ編集済み 編集者: yayadon 編集日時 2008-02-20 04:26 ]
1

スキルアップ/キャリアアップ(JOB@IT)