Alex Yakhnin
IntelliProg, Inc. .gif)
March 2003
対象 :
Microsoft® .NET Compact Framework
Microsoft® Visual Studio® .NET 2003
概要 : 構造体をバイト配列としてマーシャリングする方法を説明します。
ProcessManager.msi のダウンロード
目次
背景
バイト配列としての構造体とは?
プロセスの処理
結果の表示
まとめ
背景
.NET Compact Framework は、マネージ インターフェイスを Windows CE API の重要なセットに提供しますが、 その一部は提供しません。 この問題は Platform Invoke (P/Invoke) サービスによって解決できます。 P/Invoke は、マネージ コードが Windows CE API の関数などのアンマネージ関数を呼び出せるようにするサービスです。
P/Invoke は公開された関数の所在を確認し、呼び出します。 そして、必要に応じて、 その関数の引数 (整数値、文字列、配列、構造体など) を処理の境界を超えてマーシャリングします。 .NET Compact Framework のマーシャリング サポートは、 完全な .NET Framework で利用できるサブセットです。 たとえば、.NET Compact Framework の共通言語ランタイムは、 構造体または参照型内のオブジェクトをマーシャリングできません。 これはディープ マーシャリングと呼ばれます。 ただし、構造体に単純型が含まれているときは、 アンマネージ コードがその構造体に適合できる場合、 それらの構造体をマーシャリングできます。 したがって、ネイティブ API 関数が、 他の構造体、文字列、またはその他の高速転送型でない型で入れ子になった構造体、 またはポインタを含む複雑な構造体を要求する場合、 構造体をバイト配列に変換し、 それらを引数としてネイティブ関数に渡すことができます。 この操作は単純に構造体の宣言を C ヘッダー ファイルから変換するよりも少し手間がかかりますが、 特に、必要なネイティブ関数の呼び出しが、 必要な機能を実行する唯一の手段である場合などには、 結果的には価値のある作業です。
バイト配列としての構造体とは?
構造体とは何でしょうか? 最も一般的な形式では、 構造体は 1 つの名前に変数をまとめたものです。 これらの変数は型が異なる場合があり、 各変数が名前を持ち、 構造体からその変数を選択するときにその名前を使用します。 たとえば、POINT 構造体は、以下のアンマネージ定義を持っています。
typedef struct tagPOINT
{
LONG x;
LONG y;
}POINT;
構造体は、 宣言ごとにフォーマットされた、 一連のデータを保持する連続的なメモリ ブロックにすぎません。 アンマネージ LONG データ型は、4 バイトのメモリを使用することがわかっています。 このため、上記の POINT の例では、 x メンバと y メンバに割り当てられるメモリ内の 8 個の連続するバイトに 1 つのアドレスを持つことになります。 このことからもう 1 つの問題が生じます。 LONG データ型の 4 バイトのメモリで、 最上位バイト (MSB) はどのバイトになるでしょうかか? 最下位バイト (LSB) はどのバイトになるでしょうか? これらの質問に対する答えは MSDN で参照できます。 その資料は「Windows was designed around the little-endian architecture」で、 これは "下位バイトが最初に格納される" ことを意味します。 0x1234 のような数値は連続する 2 バイトとしてメモリに格納されます。 最初のバイトは、下位バイト (0x34) を保持し、2 番目のバイトが上位バイト (0x12) を保持します。
プロセスの処理
Windows CE プラットフォームには、非常に役立つ DLL があります。 その DLL は toolhelp.dll で、 デバイス内で実行されているプロセス内のプロセスとスレッドに関する情報を取得できる、一連の API を実装します。 たとえば、CreateToolhelp32Snapshot 関数は、 そのプロセスで使用されるプロセス、ヒープ、モジュール、およびスレッドのスナップショットを取ります。 システムのスナップショットの最初のプロセスに関する情報を取得するには、 Process32First を使用し、 その後順次 Process32Next を使用します。 これらの関数は、 以下のアンマネージの定義を持つ PPROCESSENTRY32 構造体に、 プロセスに関する情報を返します。
typedef struct tagPROCESSENTRY32
{
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID;
DWORD th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID;
LONG pcPriClassBase;
DWORD dwFlags;
TCHAR szExeFile[MAX_PATH];
DWORD th32MemoryBase;
DWORD th32AccessKey;
} PROCESSENTRY32;
この構造体のすべてのメンバは、TCHAR 配列 (szExeFile) を除いて高速転送型です。 したがって、この構造体は .NET Compact Framework によって自動的にマーシャリングされません。 ここで、構造体は単なるバイト配列であるという知識が役立ちます。 この構造体をバイト配列に変換し、Windows CE API に渡すことができます。
PROCESSENTRY32 クラスを作成し、 この構造体で定義された各メンバのオフセットを挿入することから始めましょう。
private class PROCESSENTRY32
{
// 構造体の定義の定数
private const int SizeOffset = 0;
private const int UsageOffset = 4;
private const int ProcessIDOffset=8;
private const int DefaultHeapIDOffset = 12;
private const int ModuleIDOffset = 16;
private const int ThreadsOffset = 20;
private const int ParentProcessIDOffset = 24;
private const int PriClassBaseOffset = 28;
private const int dwFlagsOffset = 32;
private const int ExeFileOffset = 36;
private const int MemoryBaseOffset = 556;
private const int AccessKeyOffset = 560;
private const int Size = 564; // 構造体の全体の大きさ
private const int MAX_PATH = 260;
// データ メンバ
public uint dwSize;
public uint cntUsage;
public uint th32ProcessID;
public uint th32DefaultHeapID;
public uint th32ModuleID;
public uint cntThreads;
public uint th32ParentProcessID;
public int pcPriClassBase;
public uint dwFlags;
public string szExeFile;
public uint th32MemoryBase;
public uint th32AccessKey;
}
これらの単純データ型をバイト配列に変換する方法はいくつかあります。 その中から System.BitConverter クラスを使用してみます。 このクラスは基本データ型をバイトの配列に変換し、 バイト配列を基本データ型に変換します。 なお、このクラスの実装は、 データが任意のデバイスに格納されるときのバイトの並び順 ("endianess") に注意する必要があります。 そこで、以下の役立つ関数を PROCESSENTRY32 クラスに追加できます。
// 使用目的 : バイト配列から uint を取得します。
private static uint GetUInt(byte[] aData, int Offset)
{
return BitConverter.ToUInt32(aData, Offset);
}
// 使用目的 : バイト配列に uint を設定します。
private static void SetUInt(byte[] aData, int Offset, int Value)
{
byte[] buint = BitConverter.GetBytes(Value);
Buffer.BlockCopy(buint, 0, aData, Offset, buint.Length);
}
// 使用目的 : バイト配列から ushort を取得します。
private static ushort GetUShort(byte[] aData, int Offset)
{
return BitConverter.ToUInt16(aData, Offset);
}
// 使用目的 : バイト配列に ushort を設定します。
private static void SetUShort(byte[] aData, int Offset, int Value)
{
byte[] bushort = BitConverter.GetBytes((short)Value);
Buffer.BlockCopy(bushort, 0, aData, Offset, bushort.Length);
}
さらに、文字列データ型の変換には、System.Text.Encoding クラスを使用できます。
// 使用目的 : バイト配列から Unicode 文字列を取得します。
private static string GetString(byte[] aData, int Offset, int Length)
{
String sReturn = Encoding.Unicode.GetString(aData, Offset, Length);
return sReturn;
}
// 使用目的 : バイト配列に Unicode 文字列を設定します。
private static void SetString(byte[] aData, int Offset, string Value)
{
byte[] arr = Encoding.ASCII.GetBytes(Value);
Buffer.BlockCopy(arr, 0, aData, Offset, arr.Length);
}
これで、PROCESSENTRY32 クラスにコンストラクタを追加できるようになりました。 このコンストラクタは、バイト配列を受け取り、"仮想" 構造体のすべてのメンバを設定します。
// バイト配列に基づいて PROCESSENTRY インスタンスを作成します。
public PROCESSENTRY32(byte[] aData)
{
dwSize = GetUInt(aData, SizeOffset);
cntUsage = GetUInt(aData, UsageOffset);
th32ProcessID = GetUInt(aData, ProcessIDOffset);
th32DefaultHeapID = GetUInt(aData, DefaultHeapIDOffset);
th32ModuleID = GetUInt(aData, ModuleIDOffset);
cntThreads = GetUInt(aData, ThreadsOffset);
th32ParentProcessID = GetUInt(aData, ParentProcessIDOffset);
pcPriClassBase = (long) GetUInt(aData, PriClassBaseOffset);
dwFlags = GetUInt(aData, dwFlagsOffset);
szExeFile = GetString(aData, ExeFileOffset, MAX_PATH);
th32MemoryBase = GetUInt(aData, MemoryBaseOffset);
th32AccessKey = GetUInt(aData, AccessKeyOffset);
}
API 参照は、dwSize メンバを PROCESSENTRY32 構造体のサイズに設定することを提案します。 それ以外の場合、Process32First の呼び出しは失敗します。 このため、ToByteArray メソッドは必要なメモリの割り当て、 および dwSize メンバの設定には注意する必要があります。
// 初期化したデータ配列を作成します。
public byte[] ToByteArray()
{
byte[] aData;
aData = new byte[Size];
// Size メンバを設定します。
SetUInt(aData, SizeOffset, Size);
return aData;
}
PROCESSENTRY32 クラスにいくつかのプロパティを追加して、 最も興味深いメンバの一部を公開しましょう。
public string Name
{
get
{
return szExeFile.Substring(0, szExeFile.IndexOf('\0'));
}
}
public ulong PID
{
get
{
return th32ProcessID;
}
}
public ulong BaseAddress
{
get
{
return th32MemoryBase;
}
}
public ulong ThreadCount
{
get
{
return cntThreads;
}
}
もちろん、 P/Invoke 関数の宣言を忘れないようにする必要があります。
private const int TH32CS_SNAPPROCESS = 0x00000002;
[DllImport("toolhelp.dll")]
public static extern IntPtr CreateToolhelp32Snapshot(uint flags, uint processid);
[DllImport("toolhelp.dll")]
public static extern int CloseToolhelp32Snapshot(IntPtr handle);
[DllImport("toolhelp.dll")]
public static extern int Process32First(IntPtr handle, byte[] pe);
[DllImport("toolhelp.dll")]
public static extern int Process32Next(IntPtr handle, byte[] pe);
[DllImport("coredll.dll")]
private static extern IntPtr OpenProcess(int flags, bool fInherit, int PID);
private const int PROCESS_TERMINATE = 1;
[DllImport("coredll.dll")]
private static extern bool TerminateProcess(IntPtr hProcess, uint ExitCode);
[DllImport("coredll.dll")]
private static extern bool CloseHandle(IntPtr handle);
private const int INVALID_HANDLE_VALUE = -1;
次に、Process クラスを追加します。 このクラスは先ほど作成した PROCESSENTRY32 クラスの機能を利用し、 クライアントから Windows CE API を呼び出すという複雑な操作をなくします。
public class Process
{
private string processName;
private IntPtr handle;
private int threadCount;
private int baseAddress;
// 既定のコンストラクタ
public Process()
{
}
// プライベート ヘルパ コンストラクタ
private Process(IntPtr id, string procname, int threadcount, int baseaddress)
{
handle = id;
processName = procname;
threadCount = threadcount;
baseAddress = baseaddress;
}
public static Process[] GetProcesses()
{
// 一時的な ArrayList
ArrayList procList = new ArrayList();
IntPtr handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if ((int)handle > 0)
{
try
{
PROCESSENTRY32 peCurrent;
PROCESSENTRY32 pe32 = new PROCESSENTRY32();
// バイト配列を取得して API 呼び出しに渡します。
byte[] peBytes = pe32.ToByteArray();
// 最初のプロセスを取得します。
int retval = Process32First(handle, peBytes);
while(retval == 1)
{
// バイトをクラスに変換します。
peCurrent = new PROCESSENTRY32(peBytes);
// Process クラスの新しいインスタンス
Process proc = new Process(new IntPtr((int)peCurrent.PID),
peCurrent.Name, (int)peCurrent.ThreadCount,
(int)peCurrent.BaseAddress);
procList.Add(proc);
retval = Process32Next(handle, peBytes);
}
}
catch(Exception ex)
{
throw new Exception("例外 : " + ex.Message);
}
// ハンドルを閉じます。
CloseToolhelp32Snapshot(handle);
return (Process[])procList.ToArray(typeof(Process));
}
else
{
throw new Exception("スナップショットを作成できません");
}
}
}
上記のコードでは、GetProcesses メソッドが、 アンマネージ関数の CreateToolhelp32Snapshot、Process32First 、 および Process32Next を呼び出すことでプロセス一覧の配列を設定しています。 このメソッドは、次の関数を呼び出すことで必要なメモリを構造体に割り当てます。
byte[] peBytes = pe32.ToByteArray();
その後、次のように Process32First 関数および Process32Next 関数に渡します。
int retval = Process32First(handle, peBytes);
これらの API で 2 つ目のパラメータを byte[] として宣言したので、 マーシャリングする場合は、 割り当てたメモリ ブロックを固定し、 このメモリのポインタをネイティブ関数に渡します。 Process32First 関数および Process32Next 関数は、 このメモリ ブロックにプロセスに関するデータを設定するので、 残りの操作はバイト配列から適切な値を抽出することです。
// バイトをクラスに変換します。
peCurrent = new PROCESSENTRY32(peBytes);
// Process クラスの新しいインスタンス
Process proc = new Process(new IntPtr((int)peCurrent.PID),
peCurrent.Name, (int)peCurrent.ThreadCount,
(int)peCurrent.BaseAddress);
明示的に、 プロパティ宣言を追加して、 Process クラスを使用するクライアントが利用できる値を作成する必要があります。
// ListBox バインディングへの ToString の実装
public override string ToString()
{
return processName;
}
public int BaseAddress
{
get
{
return baseAddress;
}
}
public int ThreadCount
{
get
{
return threadCount;
}
}
public IntPtr Handle
{
get
{
return handle;
}
}
public string ProcessName
{
get
{
return processName;
}
}
public int BaseAddess
{
get
{
return baseAddress;
}
}
結果の表示
クライアントのアプリケーションでの Process クラスの使用は、 非常に簡単な作業です。 ListBox と、 cmdRefresh と cmdEndTask という名前の 2 つの Button をフォームに追加します。 その後、フォームのコンストラクタ InitializeComponent のすぐ後にコードを 1 行追加しました。
public Form1()
{
//
// Windows フォーム デザイナのサポートに必要な関数
//
InitializeComponent();
// プロセス一覧を設定します。
lstProcess.DataSource = Process.GetProcesses();
}
Process クラスの ToString() メソッドをオーバーライドしたので、 ListBox のバインディング ロジックは DisplayMember として ProcessName を取得します。
ボタンの Click イベントによって実行されるコードは、同様に明確なものです。
private void cmdEndTask_Click(object sender, System.EventArgs e)
{
// 選択したプロセスを取得します。
Process proc = (Process)lstProcess.SelectedItem;
proc.Kill();
// プロセス一覧を最新情報に更新します。
lstProcess.DataSource = null;
lstProcess.DataSource = Process.GetProcesses();
}
private void cmdRefresh_Click(object sender, System.EventArgs e)
{
lstProcess.DataSource = null;
lstProcess.DataSource = Process.GetProcesses();
}
ユーザーが lstProcess ListBox 内の項目を選択すると、 フォーム上の Label コントロールに、Process ハンドル、BaseAddress 値および ThreadCount の値を表示します。
.gif)
図 1. 最終的なフォーム
private void lstProcess_SelectedIndexChanged(object sender,
System.EventArgs e)
{
// 選択したプロセスを取得します。
Process proc = (Process)lstProcess.SelectedItem;
// ラベルを設定します。
lblId.Text = "Process ID: " + proc.Handle;
lblThreadCount.Text = "Thread Count: " + proc.ThreadCount;
lblBaseAddress.Text = "Base Address: " + proc.BaseAddess;
}
まとめ
すべてのマーシャリングの事例が .NET Compact Framework に明確にサポートされていない場合でも、 依然としてより洗練された作業を行うことは可能です。 開発者はネイティブ構造体のラッパー クラスを作成でき、 それらをバイト配列に変換して、すべてのマーシャリングの事例を可能にします。