10/14の記事でWin9x系へのサポートについて言及した他プロセスのアドレス空間へのアクセスですが、一々実行時にプラットフォームを考えて処理を分岐するというのは死ぬほど面倒なものがあります。
そこで作ってみたのが、プラットフォームに応じて自動的に確保や読み書きをやってくれる共有メモリのラッパクラスです。飽くまでも扱いやすさのためのクラスですから、速度効率的にはそう良くないでしょう。virtualとか使ってるし。ポイントは透過的に扱えるという点なのでそんなのはまあ無視です。
ついでと言ってはなんですが、扱いやすさついでに、構造体・バイト配列・文字列・ポインタ(IntPtr)の読み書きも一元的に扱えるようにしましょう。一々「文字列を書き込むから、一旦Encoding.GetBytesでバイト配列に……うわこれも9x系はEncoding.Defaultで」とか「Marshall.PtrToStructureで……ああVSがあれば引数のインテリセンスが」とかなどの混乱無く、書き込むときはWriteのオーバーロードで、読み出すときはReadHogehogeで一発解決です。ついでに、大きめに領域を取って前半に構造体を、後半に関数などによって格納される文字列のバッファを割り当てるってなやり方のために、それぞれにアドレスオフセットを取るオーバーロードも作っておきます。このクラスを使えば確保の手間もないわけですから文字列バッファをまた別に確保しても良いんでけど、そこはまあ効率とかあるかも知れないので。
それでは共有メモリ管理クラス・SharedMemoryの解説に入りましょう。今回はじっくり解説してみます。
SharedMemoryは抽象クラス(abstract/MustInherit)で、このクラスのインスタンスを直接は作成しません。静的メソッドとしてAllocが用意されており、そこで動作プラットフォームに応じて、SharedMemoryにネストして定義されているクラスSharedMemoryNTまたはSharedMemory9xのインスタンスを作成しそれを返します。SharedMemory9xではAllocの引数のうちプロセスに関するものは使用されませんが、両方のプラットフォームから透過的に扱えるのが目標なので、プロセスに関する引数の省略を許しません。
SharedMemoryは書き込みに関するメソッドをオーバーロードで8つ、読み出しに関するメソッドをオーバーロードを含めて8つ用意しています。このうち読み書き4つずつは「型インスタンス・バイト配列・文字列・IntPtrで表現されるメモリ領域」のそれぞれの単純な読み書きを行い、仮想メソッド(virtual/Overridable)として宣言されています。残りの4つずつは書き込むアドレスのオフセットを指定できるオーバーロードになっており、こちらは抽象メソッド(abstract/MustOverride)で、派生クラスであるSharedMemoryNT/SharedMemory9xが実装を持ちます。ちなみに仮想メソッドの8つは、単純にオフセット0で対応するオーバーロードを呼び出しているだけです。一応、拡張性のため仮想メソッドで別のやり方も許容するようにしていますが、SharedMemoryNT/SharedMemory9xではそのままにしています。
確保した共有メモリが確実に解放されるように、SharedMemoryは解放手段として多少複雑な構成を取ります。まず、SharedMemoryはIDisposableインターフェイスを実装します。分かりやすさのために、Free()メソッドも定義し、実装はこちらで行います(C#では、Dispose()はそのままFree()を呼び出します(Dispose()そのものは明示的実装とし、外部から直接は見えない)。VB.NETではDispose()の別名定義としてFree()を実装します)。Free()は、まず抽象プロテクトメソッドであるFree(bool)を呼び出します。この引数のbool値はFree()から呼び出したかどうか(ファイナライザが呼び出した場合はfalse)を表します。派生クラスはこのFree(bool)をオーバーライドして共有メモリやその他のハンドルなどを解放します。Free()は更にDispose(bool)を呼び出し、SharedMemory抽象クラス側で必要な後処理を行います。
GetAddressメソッドは始めに確保した領域の先頭アドレスからオフセットを追加したアドレスを返すメソッドです。当然の事ながらオフセットを0未満にすると例外を投げます。
Addressプロパティは、確保したアドレスを利用者が取得するためのプロパティです。これを利用してプロセス越しのSendMessageをしたりするわけですね。
それから、IsNT静的プロパティは、そのままEnvironment.OSVersion.Platformを見て判断しているだけです。ところで.NET 2.0でもWin64NTとか追加されていないんですが(代わりにUnixなんてのが追加されている)どうするんでしょうね。というかWin64下でAPI呼び出しがどうなるのか知らないんですけど。DLLの名前とか。またThrowLastWin32ErrorメソッドはAPI呼び出しの失敗時に例外を送出するメソッドです。
しかし、ほとんどのメソッドでdisposedをチェックして例外を投げていますが、こういうのってメソッドにした方が良いんですかね? CheckDisposedとかそんなの。
続いてSharedMemoryNTクラスの解説です。NT系では他プロセスのメモリ空間を扱う場合、どのプロセスを扱うかと言う情報が必要なので、コンストラクタ引数に、確保するサイズの他にそのプロセスのハンドルまたはSystem.Diagnostics名前空間のProcessインスタンスを要求します。Processインスタンスを使って確保する場合、そのProcessインスタンスのClose/Disposeが呼ばれるとハンドルが無効になってしまうため、与えられたProcessインスタンスのIdプロパティを元に新しくProcessインスタンスを作成します。
読み書きを行うメソッドの内、抽象メソッドをそれぞれオーバーライドして動作を規定します。WriteProcessMemory/ReadProcessMemoryを使う他はそう目立ったところはないでしょう。
SharedMemory9xは更に簡単です。確保するとき以外は自プロセスのIntPtrへの操作と同じですからね。
ところで、WinFXではこの辺はどうなるんでしょうか。タスクバーからアイコンを消したいとか良くある要求だと思うんですが。そうでもないですか。
今回は作業量の関係上C#のコードのみとなっています。一応そのうちVB.NETのコードも書く予定ですが。いつものXMLドキュメント付きのですが、これってただ見づらいだけかなぁ……。
using System; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; namespace HongliangSoft.Utilities { public abstract class SharedMemory : IDisposable { private IntPtr address; private int allocSize; public IntPtr GetAddress(int offset) { this.ValidateArguments(0, offset); return new IntPtr(address.ToInt64() + offset); } protected void SetAddress(IntPtr address) { if (disposed) throw new ObjectDisposedException( "", "破棄されたインスタンスを再利用することはできません。"); if (!address.Equals(IntPtr.Zero)) throw new InvalidOperationException( "共有メモリのアドレスを再設定することはできません。"); this.address = address; } public IntPtr Address { get { this.ValidateArguments(0, 0); return address; } } protected void ThrowLastWin32Error() { int error = Marshal.GetLastWin32Error(); StringBuilder buffer = new StringBuilder(256); FormatMessage(FormatMessageOptions.FromSystem, IntPtr.Zero, error, 0, buffer, 256, IntPtr.Zero); throw new Win32Exception(error, buffer.ToString()); } public static SharedMemory Alloc(IntPtr process, int size) { SharedMemory memory; if (IsNT) memory = new SharedMemoryNT(process, size); else memory = new SharedMemory9x(size); memory.allocSize = size; return memory; } public static SharedMemory Alloc(Process process, int size) { SharedMemory memory; if (IsNT) memory = new SharedMemoryNT(process, size); else memory = new SharedMemory9x(size); memory.allocSize = size; return memory; } public virtual void Write(object value) { this.Write(value, 0); } public abstract void Write(object value, int offset); public virtual void Write(byte[] value) { this.Write(value, 0); } public abstract void Write(byte[] value, int offset); public virtual void Write(IntPtr value, int size) { this.Write(value, size, 0); } public abstract void Write(IntPtr value, int size, int offset); public virtual void Write(string value) { this.Write(value, 0); } public abstract void Write(string value, int offset); public virtual object ReadStructure(Type type) { return this.ReadStructure(type, 0); } public abstract object ReadStructure(Type type, int offset); public virtual byte[] ReadBytes(int size) { return this.ReadBytes(size, 0); } public abstract byte[] ReadBytes(int size, int offset); public virtual string ReadString(int size) { return this.ReadString(size, 0); } public abstract string ReadString(int size, int offset); public virtual void ReadToPtr(IntPtr dest, int size) { this.ReadToPtr(dest, size); } public abstract void ReadToPtr(IntPtr dest, int size, int offset); void IDisposable.Dispose() { this.Free(); } public void Free() { this.Free(true); this.Dispose(true); GC.SuppressFinalize(this); } protected abstract void Free(bool disposing); private void Dispose(bool disposing) { if (!this.disposed) { this.disposed = true; this.address = IntPtr.Zero; } } ~SharedMemory() { this.Free(false); this.Dispose(false); } protected virtual void ValidateArguments(int size, int offset) { if (this.disposed) throw new ObjectDisposedException( "", "破棄された共有メモリにはアクセスできません。"); if (offset < 0) throw new ArgumentOutOfRangeException( "offset", offset, "オフセットを0未満には指定できません。"); if ((offset + size) > this.allocSize) throw new ArgumentException( "offsetとsizeの合計が、確保したサイズを超えてしまっています。" + "確保した以外の領域にアクセスしてしまいます。"); } private bool disposed = false; protected static bool IsNT { get { return Environment.OSVersion.Platform == PlatformID.Win32NT; } } [DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)] private static extern int FormatMessage( FormatMessageOptions flags, IntPtr source, int messageId, int langId, StringBuilder buffer, int size, IntPtr arguments); [Flags] private enum FormatMessageOptions { MaxWidthMask = 0x00FF, AllocateBuffer = 0x0100, IgnoreInserts = 0x0200, FromString = 0x0400, FromHModule = 0x0800, FromSystem = 0x1000, ArgumentArray = 0x2000, } public sealed class SharedMemoryNT : SharedMemory { internal SharedMemoryNT(IntPtr process, int size) { this.AllocInternal(process, size); } internal SharedMemoryNT(Process process, int size) { this.processInstance = Process.GetProcessById(process.Id); this.AllocInternal(processInstance.Handle, size); } private void AllocInternal(IntPtr process, int size) { if (process.Equals(IntPtr.Zero)) throw new ArgumentException( "process", "指定したプロセスハンドルは無効です。"); IntPtr address = VirtualAllocEx(process, IntPtr.Zero, size, AllocationTypes.Alloc, ProtectTypes.ExecuteReadWrite); if (address.Equals(IntPtr.Zero)) base.ThrowLastWin32Error(); this.process = process; base.SetAddress(address); } public override void Write(object value, int offset) { int size = Marshal.SizeOf(value); IntPtr ptr = Marshal.AllocCoTaskMem(size); try { Marshal.StructureToPtr(value, ptr, true); this.Write(ptr, size, offset); } finally { Marshal.FreeCoTaskMem(ptr); } } public override void Write(byte[] value, int offset) { base.ValidateArguments(value.Length, offset); if (!WriteProcessMemory(this.process, base.GetAddress(offset), value, value.Length, IntPtr.Zero)) base.ThrowLastWin32Error(); } public override void Write(IntPtr value, int size, int offset) { base.ValidateArguments(size, offset); if (!WriteProcessMemory(this.process, base.GetAddress(offset), value, size, IntPtr.Zero)) base.ThrowLastWin32Error(); } public override void Write(string value, int offset) { this.Write(Encoding.Unicode.GetBytes(value), offset); } public override object ReadStructure(Type type, int offset) { int size = Marshal.SizeOf(type); IntPtr buffer = Marshal.AllocCoTaskMem(size); try { this.ReadToPtr(buffer, size, offset); return Marshal.PtrToStructure(buffer, type); } finally { Marshal.FreeCoTaskMem(buffer); } } public override byte[] ReadBytes(int size, int offset) { base.ValidateArguments(size, offset); byte[] buffer = new byte[size]; if (!ReadProcessMemory(this.process, base.GetAddress(offset), buffer, size, IntPtr.Zero)) base.ThrowLastWin32Error(); return buffer; } public override string ReadString(int size, int offset) { IntPtr buffer = Marshal.AllocCoTaskMem(size); try { this.ReadToPtr(buffer, size, offset); return Marshal.PtrToStringUni(buffer); } finally { Marshal.FreeCoTaskMem(buffer); } } public override void ReadToPtr(IntPtr dest, int size, int offset) { base.ValidateArguments(size, offset); if (!ReadProcessMemory(this.process, base.GetAddress(offset), dest, size, IntPtr.Zero)) base.ThrowLastWin32Error(); } protected override void Free(bool disposing) { if (!disposed) { VirtualFreeEx(this.process, this.address, 0, FreeTypes.Release); process = IntPtr.Zero; if (disposing) { if (processInstance != null) { processInstance.Dispose(); processInstance = null; } } } } private IntPtr process; private Process processInstance; [DllImport("kernel32.dll", SetLastError=true)] private static extern IntPtr VirtualAllocEx( IntPtr process, IntPtr address, int size, AllocationTypes allocationType, ProtectTypes protect); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool VirtualFreeEx( IntPtr process, IntPtr address, int size, FreeTypes freeType); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool WriteProcessMemory( IntPtr process, IntPtr address, IntPtr buffer, int size, IntPtr writtenSize); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool WriteProcessMemory( IntPtr process, IntPtr address, [In] byte[] buffer, int size, IntPtr writtenSize); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool ReadProcessMemory( IntPtr process, IntPtr address, IntPtr buffer, int size, IntPtr readSize); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool ReadProcessMemory( IntPtr process, IntPtr address, [Out] byte[] buffer, int size, IntPtr readSize); [Flags] private enum AllocationTypes { Commit = 0x00001000, Reserve = 0x00002000, Alloc = Commit | Reserve, Reset = 0x00080000, TopDown = 0x00100000, } [Flags] private enum ProtectTypes { NoAccess = 0x0001, ReadOnly = 0x0002, ReadWrite = 0x0004, Execute = 0x0010, ExecuteRead = 0x0020, ExecuteReadWrite = 0x0040, Guard = 0x0100, NoCache = 0x0200, } [Flags] private enum FreeTypes { Decommit = 0x4000, Release = 0x8000, } } public sealed class SharedMemory9x : SharedMemory { internal SharedMemory9x(int size) { this.map = CreateFileMapping(InvalidHandleValue, IntPtr.Zero, ProtectAttributes.Commit, 0, size, null); if (this.map.Equals(IntPtr.Zero)) base.ThrowLastWin32Error(); base.SetAddress(MapViewOfFile(this.map, AccessMode.AllAccess, 0, 0, 0)); if (base.Address.Equals(IntPtr.Zero)) { CloseHandle(this.map); base.ThrowLastWin32Error(); } } public override void Write(object value, int offset) { base.ValidateArguments(Marshal.SizeOf(value), offset); Marshal.StructureToPtr(value, base.GetAddress(offset), true); } public override void Write(byte[] value, int offset) { base.ValidateArguments(value.Length, offset); Marshal.Copy(value, 0, base.GetAddress(offset), value.Length); } public override void Write(IntPtr value, int size, int offset) { base.ValidateArguments(size, offset); CopyMemory(base.GetAddress(offset), value, size); } public override void Write(string value, int offset) { this.Write(Encoding.Default.GetBytes(value), offset); } public override object ReadStructure(Type type, int offset) { base.ValidateArguments(Marshal.SizeOf(type), offset); return Marshal.PtrToStructure(base.GetAddress(offset), type); } public override byte[] ReadBytes(int size, int offset) { base.ValidateArguments(size, offset); byte[] bytes = new byte[size]; Marshal.Copy(base.GetAddress(offset), bytes, 0, size); return bytes; } public override string ReadString(int size, int offset) { base.ValidateArguments(size, offset); return Marshal.PtrToStringAnsi(base.GetAddress(offset)); } public override void ReadToPtr(IntPtr dest, int size, int offset) { base.ValidateArguments(size, offset); CopyMemory(dest, base.GetAddress(offset), size); } protected override void Free(bool disposing) { if (!disposed) { UnmapViewOfFile(base.Address); CloseHandle(map); map = IntPtr.Zero; } } private IntPtr map; [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)] private static extern IntPtr CreateFileMapping( IntPtr fileHandle, IntPtr security, ProtectAttributes protects, int maximumSizeHigh, int maximumSizeLow, string name); [DllImport("kernel32.dll", SetLastError=true)] private static extern IntPtr MapViewOfFile( IntPtr mappingObject, AccessMode desiredAccess, int offsetHigh, int offserLow, int numberOfBytesToMap); [DllImport("kernel32.dll")] private static extern void CopyMemory( IntPtr destination, IntPtr source, int length); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool UnmapViewOfFile(IntPtr baseAddress); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool CloseHandle(IntPtr handle); private enum AccessMode { Copy = 0x00000001, Write = 0x00000002, Read = 0x00000004, AllAccess = 0x000F001F, } [Flags] private enum ProtectAttributes { ReadOnly = 0x00000002, ReadWrite = 0x00000004, WriteCopy = 0x00000008, Image = 0x01000000, Reserve = 0x04000000, Commit = 0x08000000, NoCache = 0x10000000, } private static readonly IntPtr InvalidHandleValue = new IntPtr(-1); } } }
.NET 2.0 では、SafeHandle 派生型を使って寿命管理を行うのがベターですかね。
https://www.microsoft.com/japan/msdn/msdnmag/issues/05/10/Reliability/default.aspx
http://msdn2.microsoft.com/ja-jp/library/ms228970.aspx
リソースハンドルとして生の IntPtr を保持するというシナリオは、.NET 2.0 になって大分減ったと思います。
正直 SafeHandle の使い方に微妙にとまどっています。えーと、要は今まで IntPtr 使ってたところの代わりに SafeHandle 派生クラスを使えばいいってだけなんですよね。使用者の視点から見たら。で、オーバーライドする ReleaseHandle メソッドで解放すると。
ちょっと勉強してきます(^^;
んーと SharedMemory クラスを CriticalFinalizerObject から派生させるということでしょうか?
だとしたらやはり問題が残るかと。
MSDN Magazine の記事にも書いてありますが、1つはメソッド戻り値で IntPtr を受ける方法だと、メソッドリターンから変数への代入の間に非同期例外が発生したときにハンドルを受け取り損ねるという問題があります。
受け取り損ねた IntPtr は値型であるため、永遠にロストします。
『.NET Framework の信頼性機能でコードを実行し続ける』
>>残念ながら、非同期例外が、FindFirstFile のリターン後から、取得した IntPtr ハンドルの格納後までの間に発生した場合、そのオペレーティング システム リソースは、開放の望みをほとんど絶たれたメモリ リークとなりますが、ここで救いとなるのが SafeHandle です。
>>.NET Framework 2.0 では、この宣言部分を次のように書き直すことができます。
ポイントは CLR の標準マーシャラが SafeHandle (とその派生型)へのマーシャリングを「知っている」ため、これらとして受け取ることで、たとえ変数に代入し損ねても CriticalFinalizer は実行されるということです。(CriticalFinalizerObject は当然参照型)
『.NET Framework の信頼性機能でコードを実行し続ける』
>>内部的には、.NET Framework は、大量の SafeHandle 派生の型を使用します (それぞれ、処理を必要とするアンマネージド リソースの各型用です)が、外からは、SafeFileHandle (ファイル ハンドルのラップに使用) と SafeWaitHandle (同期ハンドルのラップに使用) 等、数個しか見ることができません。
とあるように、Microsoft はちゃっかり大量の書き換えを行っているようです。
>とあるように、Microsoft はちゃっかり大量の書き換えを行っているようです。
.NET Framework 2.0 から、「IDisposable だけどファイナライザは持たない」というクラスがちらほらと増えています。
ファイナライザで確実なリソースの解放を行うという責務をより小さいクラス(SafeHandle 派生型)に移せるなら、わざわざ大きなクラスにファイナライザを実装する必要はないという考え方ですな。
あー、完全にうっかりしてました。
確かにこの問題がありましたね。
>ファイナライザで確実なリソースの解放を行うという責務をより小さいクラス(SafeHandle 派生型)に移せるなら、わざわざ大きなクラスにファイナライザを実装する必要はないという考え方ですな。
この考えには私は諸手をあげて賛成しますね。
以前から IntPtr を Dispose したかった身としては、SafeHandle はなかなか理想的な解になっていると思います。
今まで書きためたコードを見直すのが面倒ですが(^^;
より確実性・安全性が増すのは喜ばしいですが、互換コード書くのが大変になるのは宿命ですねー。