Win32 ライブラリとその他のライブラリの使用
Eric Gunnerson
Microsoft Corporation
September 16, 2002
MSDN Code Center からサンプル ファイルをダウンロードする (csharp09192002_sample.exe)。
前回のコラムでは、 C# 内から既存のコードを使用するさまざまな方法の概要を説明しました。 今回は、 コード内から Win32® ライブラリやその他既存のライブラリを使用する方法について調査するつもりです。
C# ユーザーからの共通する 2 つの質問に、 「なぜ Windows® に組み込まれているものを使用するために特別なコードを記述する必要があるのでしょうか?」 と「なぜこれを行うためのものがフレームワークには存在しないのですか?」 というのがあります。 フレームワーク チームが .NET のフレームワーク部分を構築していたときに、 .NET プログラマのために Win32 を利用できるようにする必要があること、 および Win32 API セットが *膨大* であることがわかっていました。 しかし、チームにはすべての Win32 のマネージ インターフェイスをコーディングし、 テストし、ドキュメント化するリソースがありませんでした。 そのため、最も重要なものを優先的に注目する必要がありました。 多くの一般的な操作にはマネージ インターフェイスがありますが、 Win32 のすべてのセクションを網羅しているわけではありません。
Platform Invoke (P/Invoke) は、 これを行うための最も一般的な方法です。 P/Invoke を使用するには、 関数を呼び出す方法を説明するプロトタイプを記述し、 ランタイムがこの情報を使用して、呼び出しを行います。 他にも、 C++ 用マネージ拡張を使用して関数をラップする方法もあります。 これについては、今後のコラムで説明するつもりです。
これを行う方法を理解するための最善の方法は、 例を作成することです。 場合によっては、コードの一部だけを提供しました。 完全なコードは、ダウンロードして利用できます。
簡単な例
最初の例では、 サウンドを再生するための Beep()
API を呼び出します。 まず始めに、 Beep()
の適切な定義を記述する必要があります。 MSDN で定義を調べてみたところ、 次のようなプロトタイプがありました。
BOOL Beep(
DWORD dwFreq, // サウンドの周波数
DWORD dwDuration // サウンドの継続時間
);
このコードを C# で記述するには、 Win32 の型を適切な C# の型に書き換える必要があります。 DWORD
は 4 バイト整数値なので、 C# での類似型として int
または uint
を使用できます。 int 型は CLS 準拠型 (すべての .NET 言語で使用できる) ので、 uint
型よりも一般的な選択肢です。 ただし、多くの場合その違いは重要ではありません。 bool
型は BOOL
型の類似型です。 そのため、C# では次のようなプロトタイプを記述できます。
public static extern bool Beep(int frequency, int duration);
これは、この関数の実際のコードが他の場所にあることを示すために extern を使用した以外は適正な標準定義です。 このプロトタイプは、 ランタイムが関数を呼び出す方法を指示しています。 したがって、関数を探す場所を指示する必要があります。
このため、MSDN に戻ることにしましょう。 リファレンス情報によれば、 Beep()
関数が kernel32.lib
で定義されていることがわかります。 つまり、ランタイム コードは kernel32.dll
に含まれいます。 これをランタイムに通知するために、DllImport 属性をプロトタイプに追加します。
[DllImport("kernel32.dll")]
必要な作業は以上です。 以下に、 1960 年代のお粗末な SF 映画によくあるような不協和音を生成するコードの完全な例を示します。
using System;
using System.Runtime.InteropServices;
namespace Beep
{
class Class1
{
[DllImport("kernel32.dll")]
public static extern bool Beep(int frequency, int duration);
static void Main(string[] args)
{
Random random = new Random();
for (int i = 0; i < 10000; i++)
{
Beep(random.Next(10000), 100);
}
}
}
}
このサウンドが、聞こえるところにいる人を誰でもいらいらさせることを保証します。 DllImport により Win32 内の任意のコードを呼び出すことができるので、 悪意のあるコードを記述する機会が生じます。 そのため、ランタイムは完全に信頼できるユーザーが P/Invoke 呼び出しを行うことを要求します。
列挙型と定数
Beep()
は任意のサウンドを再生するのに適しています。 しかし、特定の種類のサウンドに関連付けられるサウンドを再生したい場合もあります。 その場合、代わりに MessageBeep()
を使用します。 MSDN は次のプロトタイプを提供しています。
BOOL MessageBeep(
UINT uType // サウンドの種類
);
これは簡単にみえます。 しかし、解説を読むと、2 つの興味深い事実に気づきます。
まず、実際には uType
引数には定義済み定数のセットを使用します。
次に、指定可能な引数の値に -1 があることです。 これは、 引数が uint
で定義されていますが、 int
の方がより適しているということになります。
uType
引数に列挙型を使用することは理にかなっています。 MSDN に名前付きの定数の一覧がありますが、 それは値が何であるかというヒントにはなりません。 そのため、実際の API を調べる必要があります。
C++ がインストールされた Visual Studio® があれば、 プラットフォーム SDK は \Program Files\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include にあります。
定数を探すには、そのディレクトリで findstr コマンドを実行します。
findstr "MB_ICONHAND" *.h
定数は、winuser.h
にありました。 これを使用して、 独自の列挙型とプロトタイプを作成します。
public enum BeepType
{
SimpleBeep = -1,
IconAsterisk = 0x00000040,
IconExclamation = 0x00000030,
IconHand = 0x00000010,
IconQuestion = 0x00000020,
Ok = 0x00000000,
}
[DllImport("user32.dll")]
public static extern bool MessageBeep(BeepType beepType);
これで関数を次のように呼び出すことができるようになります。
MessageBeep(BeepType.IconQuestion);
構造体の処理
ラップトップ コンピュータの電源の状態を知りたいと考えることがあります。 Win32 は、この情報を取得する電源管理関数を提供しています。
MSDN を少し検索すると、 GetSystemPowerStatus()
関数が見つかります。
BOOL GetSystemPowerStatus(
LPSYSTEM_POWER_STATUS lpSystemPowerStatus
);
この関数は、 構造体へのポインタを受け取ります。 これは、ここまで扱ってこなかったテーマです。 この構造体を使って作業するには、 C# で構造体を定義する必要があります。 まず、以下のようなアンマネージ定義からはじめましょう。
typedef struct _SYSTEM_POWER_STATUS {
BYTE ACLineStatus;
BYTE BatteryFlag;
BYTE BatteryLifePercent;
BYTE Reserved1;
DWORD BatteryLifeTime;
DWORD BatteryFullLifeTime;
} SYSTEM_POWER_STATUS, *LPSYSTEM_POWER_STATUS;
次に、 C 言語の型を C# の型に置き換えることによって、 C# バージョンの構造体を記述します。
struct SystemPowerStatus
{
byte ACLineStatus;
byte batteryFlag;
byte batteryLifePercent;
byte reserved1;
int batteryLifeTime;
int batteryFullLifeTime;
}
その後、 C# のプロトタイプを記述することは簡単です。
[DllImport("kernel32.dll")]
public static extern bool GetSystemPowerStatus(
ref SystemPowerStatus systemPowerStatus);
このプロトタイプでは、 "ref
" を使用して、 値によって構造体を渡す代わりに、 構造体へのポインタを渡すことを示しています。 これは、 ポインタによって渡される構造体を扱う一般的な方法です。
この関数は正しく機能しますが、 ACLineStatus
フィールドと batteryFlag
フィールドを列挙型で定義する方がより適切になります。
enum ACLineStatus: byte
{
Offline = 0,
Online = 1,
Unknown = 255,
}
enum BatteryFlag: byte
{
High = 1,
Low = 2,
Critical = 4,
Charging = 8,
NoSystemBattery = 128,
Unknown = 255,
}
構造体フィールドがバイト型なので、 列挙型の基本型に byte
を使用していることに注意してください。
文字列
.NET の文字列型は 1 つしか存在しませんが、 アンマネージ環境ではいくつかの選択肢があります。 それは、 文字ポインタと埋め込み文字配列を持つ構造体で、 それぞれ適切にマーシャリングする必要があります。
Win32 では、 次の 2 つの異なる文字列表記も使用されます。
- ANSI
- Unicode
従来の Windows では、 シングル バイト文字が使用されていました。 シングル バイト文字は記憶領域を節約できますが、 多くの言語に対して複雑なマルチ バイト エンコードを持っていました。 Windows NT® の出現により、 2 バイトの Unicode エンコードが使用されるようになりました。 Win32 API はこの違いをうまく処理しています。 Win9x プラットフォームではシングル バイト文字を表し、 NT プラットフォーム上では 2 バイト Unicode 文字を表す TCHAR
型を定義しています。 文字列または文字データを含む構造体を受け取るすべての関数では、 渡される文字列または文字データ構造体が Ansi であることを示す A
サフィックスと、 ワイド文字 (すなわち Unicode) であることを示す W
サフィックスを持つ 2 つのバージョンの構造を定義します。 C++ プログラムをシングル バイトに対してコンパイルすると、 A
バリアントを、 Unicode に対してコンパイルすると、 W
バリアントを受け取ります。 Win9x プラットフォームは Ansi バージョンを保持し、 WinNT プラットフォームは W
を保持します。
P/Invoke のデザイナは、 ユーザーがどのプラットフォームで実行するかを意識しなくてすむように、 A
バージョンまたは W
バージョンのいずれかを自動的に使用する、 組み込みのサポートを提供しました。 呼び出している関数が存在しない場合、 相互運用層が A
バージョンまたは W
バージョンを検索し、 代わりに使用します。
文字列サポートにはわずかに微妙な点があります。 実例でお見せするのがよいでしょう。
単純な文字列
以下に、 文字列引数を受け取る関数の簡単な例を示します。
BOOL GetDiskFreeSpace(
LPCTSTR lpRootPathName, // ルート パス
LPDWORD lpSectorsPerCluster, // クラスタごとのセクタ数
LPDWORD lpBytesPerSector, // セクタごとのバイト数
LPDWORD lpNumberOfFreeClusters, // 空きクラスタ数
LPDWORD lpTotalNumberOfClusters // 全クラスタ数
);
ルート パスは LPCTSTR
として定義されています。 これはプラットフォームに依存しないバージョンの文字列ポインタです。
GetDiskFreeSpace()
という名前の関数は存在しないので、 マーシャラは自動的に "A
" バリアントまたは "W
" バリアントを検索し、 対応する関数を呼び出します。 属性を使用して、 マーシャラに API がどの文字列型を要求しているかを指示することになるでしょう。
以下に、 上記で定義したのと同じ関数の完全な定義を示します。
[DllImport("kernel32.dll")]
static extern bool GetDiskFreeSpace(
[MarshalAs(UnmanagedType.LPTStr)]
string rootPathName,
ref int sectorsPerCluster,
ref int bytesPerSector,
ref int numberOfFreeClusters,
ref int totalNumberOfClusters);
残念ながら、 この関数はうまく動作しませんでした。 問題は既定の処理にあります。 マーシャラはどのシステム上で実行されるかに関係なく Ansi バージョンの API を検索しようとします。 しかし、 LPTStr
は Windows NT プラットフォーム上では Unicode 文字列を使用する必要があることを意味するので、 結果的には Unicode 文字列を指定して Ansi 関数を呼び出すことになります。 これは機能しません。
この関数を機能させるには 2 通りの方法があります。 簡単な方法は、 単に MarshalAs
属性を削除するだけです。 この属性を削除すると、 常に関数の A
バージョンを呼び出すことになります。 その結果、あらゆるプラットフォームで正しく機能します。 ただし、これを行うと、 マーシャラは .NET 文字列を Unicode 型からマルチ バイトに変換し、 関数の A
バージョンを呼び出し、 文字列を Unicode に変換して戻した後、 W
バージョンの関数を呼び出します。 そのため、処理が低速になります。
これを防ぐために、 Win9x プラットフォームであれば A
バージョンを、 NT プラットフォーム上であればW
バージョンを検索することをマーシャラに指示する必要があります。 DllImport
属性の一部として CharSet
を設定することによってこれを行います。
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
私の非科学的な計測によれば、 以前のオプションよりも 5 パーセントほど速くなります。
大部分の Win32 API は、 CharSet
属性を設定し、 文字列型の LPTStr
を使用することで機能します。 ただし、何らかの理由により、 A
/W
メカニズムを使用しない関数もあります。
文字列バッファ
.NET の文字列型は不変型です。 つまり、その値は決して変化しません。 文字列バッファに文字列値をコピーする関数の場合、 文字列は機能しないでしょう。 これを行うと、 一番うまくいった場合でも、 文字列が変換されるときに、 マーシャラが作成した一時バッファが破損します。 最悪の場合は、 マネージ ヒープが破損するという、 一般的には最悪の事態を引き起こします。 いずれにせよ、正しい値は返ってこないでしょう。
これを機能させるには、 別の型を使用する必要があります。 StringBuilder
型がバッファとして機能するようにデザインされています。 これを文字列の代わりに使用します。 以下に例を示します。
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern int GetShortPathName(
[MarshalAs(UnmanagedType.LPTStr)]
string path,
[MarshalAs(UnmanagedType.LPTStr)]
StringBuilder shortPath,
int shortPathLength);
この関数を使用するのは簡単です。
StringBuilder shortPath = new StringBuilder(80);
int result = GetShortPathName(
@"d:\test.jpg", shortPath, shortPath.Capacity);
string s = shortPath.ToString();
StringBuilder
の Capacity
が、 バッファのサイズとして渡されていることに注意してください。
埋め込み文字配列を持つ構造体
一部の関数は、 埋め込み文字配列を持つ構造体を受け取ります。 たとえば、 GetTimeZoneInformation()
関数は以下の構造体へのポインタを受け取ります。
typedef struct _TIME_ZONE_INFORMATION {
LONG Bias;
WCHAR StandardName[ 32 ];
SYSTEMTIME StandardDate;
LONG StandardBias;
WCHAR DaylightName[ 32 ];
SYSTEMTIME DaylightDate;
LONG DaylightBias;
} TIME_ZONE_INFORMATION, *PTIME_ZONE_INFORMATION;
これを C# で使用するには 2 つの構造体が必要になります。 SYSTEMTIME
構造体は、 セットアップが簡単です。
struct SystemTime
{
public short wYear;
public short wMonth;
public short wDayOfWeek;
public short wDay;
public short wHour;
public short wMinute;
public short wSecond;
public short wMilliseconds;
}
驚くべきことは何もありません。 しかし、 TimeZoneInformation
の定義はやや複雑です。
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct TimeZoneInformation
{
public int bias;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string standardName;
SystemTime standardDate;
public int standardBias;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string daylightName;
SystemTime daylightDate;
public int daylightBias;
}
この定義を細かく見ると、 2 つの重要な点があります。 まず、MarshalAs
属性です。
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
ByValTStr
をドキュメントで調べると、 埋め込み文字配列を使用していることがわかります。 SizeConst
を使用して、 配列のサイズを設定しています。
最初にこれをコーディングしたとき、 実行エンジン エラーになってしまいました。 これは通常相互運用の一部として、 一部のメモリに上書きしていることを意味します。 つまり、 構造体のサイズが誤っていたことを示しています。 マーシャラが使用していたサイズを取得するために、 Marshal.SizeOf() を使用したところ、 サイズは 108 バイトでした。 少し調査してみて、 相互運用では既定の文字型が Ansi またはシングル バイトであることをすぐに思い出しました。 関数定義で WCHAR
として型が設定された文字型は 2 バイトになるので、 問題が生じます。
StructLayout
属性を追加することによって、 これを解決しました。 シーケンシャル レイアウトが構造体の既定のレイアウトです。 つまり、すべてのフィールドが一覧された順にレイアウトされます。 CharSet
値は、 常に適切な文字型を使用するように Unicode に設定されます。
この修正により関数は正しく機能しました。 なぜ、この関数で CharSet.Auto
を使用しなかったかを疑問に思うことでしょう。 これは、A
バリアントや W
バリアントを持たない関数の 1 つです。 この関数は常に Unicode 文字列を使用するので、 この方法でハード コーディングしました。
コールバックに使用する関数
Win32 関数が複数のデータ項目を返す必要があるとき、 通常、コールバック メカニズムが使用されます。 開発者は関数ポインタを関数に渡します。 その結果、列挙される項目ごとに開発者の関数が呼び出されます。
C# は、 関数ポインタの代わりにデリゲートを使用します。 デリゲートは、Win32 関数を呼び出しているときの関数ポインタの代用として使用されます。
このような関数の例に EnumDesktops()
関数があります。
BOOL EnumDesktops(
HWINSTA hwinsta, // ウィンドウ ステーションのハンドル
DESKTOPENUMPROC lpEnumFunc, // コールバック関数
LPARAM lParam // コールバック関数の値
);
HWINSTA
型は IntPtr
に置き換えられ、 LPARAM
は int に置き換えられます。 DESKTOPENUMPROC
には少し作業が必要になります。 以下に MSDN での定義を示します。
BOOL CALLBACK EnumDesktopProc(
LPTSTR lpszDesktop, // デスクトップ名
LPARAM lParam // ユーザー定義値
);
これを、次のデリゲートに変換できます。
delegate bool EnumDesktopProc(
[MarshalAs(UnmanagedType.LPTStr)]
string desktopName,
int lParam);
これを定義した後に、 EnumDesktops()
の定義を記述できます。
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern bool EnumDesktops(
IntPtr windowStation,
EnumDesktopProc callback,
int lParam);
これで、関数が呼び出され、実行されるようになります。
相互運用でデリゲートを使用する場合の重要なヒントが 1 つあります。 マーシャラはデリゲートを参照する関数ポインタを作成します。 これはアンマネージ関数に渡される関数ポインタです。 しかし、マーシャラは、アンマネージ関数が関数ポインタを使って行うことを考慮できません。 そのため、関数への呼び出しの間これが有効であることだけを想定します。
その結果、 後で使用するために関数ポインタを保存する SetConsoleCtrlHandler()
などの関数を呼び出す場合、 コードでデリゲートを参照することを保証する必要があります。 これを行わないと、 関数は機能しているように見えますが、 その後ガベージ コレクションがデリゲートを削除すると、 問題が生じることになります。
より高度な関数
これまでに示したすべての例は、 比較的単純なものですが、 より複雑な Win32 関数も多数存在します。 以下に一例を示します。
DWORD SetEntriesInAcl(
ULONG cCountOfExplicitEntries, // エントリ数
PEXPLICIT_ACCESS pListOfExplicitEntries, // バッファ
PACL OldAcl, // 元の ACL
PACL *NewAcl // 新しい ACL
);
最初の 2 つの引数は極めて簡単に処理できます。 ulong
は容易に処理でき、 バッファは UnmanagedType.LPArray
を使ってマーシャリングできます。
3 番目と 4 番目の引数にはやや問題があります。 問題は ACL
の定義方法にあります。 ACL 構造体は ACL のヘッダーを定義するだけで、 残りのバッファは ACEs
によって構成されます。 ACE
は、 ACEs
のいくつかの異なる型の 1 つになり、 これらの ACEs
は異なる長さを持ちます。
すべてのバッファを割り当て、 かなり安全ではないコードを使用することを決断すれば、 これを C# で処理することは可能です。 しかし、これは "膨大" な作業になり、 デバッグも非常に大変です。 C++ の API を使用する方がはるかに簡単です。
属性のその他のオプション
DLLImport
属性と StructLayout
属性は、 P/Invoke を使用する上で役に立つオプションをいくつか持っています。 以下に完全な一覧を用意しました。
DLLImport
CallingConvention
関数が使用する呼び出し規約をマーシャラに指示します。 ユーザーの関数の呼び出し規約にこれを設定することになるでしょう。 一般的に、この設定を誤るとコードは機能しません。 ただし、 ユーザーの関数が Cdecl
関数で、 この関数を StdCall
(既定値) を使用して呼び出す場合は、 関数は機能します。 しかし、関数の引数がスタックから削除されず、 スタック オーバーフローの原因となる可能性があります。
CharSet
A
バリアントまたは W
バリアントのどちらを呼び出すかを制御します。
EntryPoint
このプロパティを使用して、 マーシャラが DLL 内で検索する名前を設定します。
ExactSpelling
この値を true に設定すると、 マーシャラは A
と W
の検索機能をオフにします。 この属性のドキュメントは古いものです。
PreserveSig
COM 相互運用は、 COM 相互運用がその値を返したように見える最終的な出力引数を持つ関数を作成します。 このプロパティはその機能をオフにします。
SetLastError
何が起こったかを調べることができるように、 Win32 API SetLastError()
を呼び出すことを保証します。
StructLayout
LayoutKind
構造体の既定のレイアウトはシーケンシャルです。 ほとんどの場合このレイアウトで機能します。 構造体のメンバがどこに配置されるかを総合的に制御する必要がある場合、 構造体のすべてのメンバに LayoutKind.Explicit
と FieldOffset
属性を使用できます。 これは通常共用体を作成する必要がある場合使用します。
CharSet
ByValTStr
メンバの既定の文字型を管理します。
Pack
構造体のパッキング サイズを設定します。 これは構造体のアライメントを制御します。 C の構造体が異なるパッキングを使用している場合、 これを設定する必要があります。
Size
構造体のサイズを設定します。 通常は使用しませんが、 構造体の最後に余分な領域を割り当てる必要がある場合に使用できます。
異なる場所からの読み込み
実行時に DLLImport がファイルを検索する場所を指定する方法はありません。 ただし、これを行わせるために使用できる手法があります。
DllImport は、 その機能を果たすために LoadLibrary()
を呼び出します。 特定の DLL がプロセスにすでに読み込まれている場合、 読み込むために指定したパスが異なっていても、 LoadLibrary()
は正常終了します。
つまり、 LoadLibrary()
を直接を呼び出せば、 DLL をどこからでも読み込むことができ、 DllImport LoadLibrary()
はそのバージョンを使用することになります。
この動作により、 異なる DLL を呼び出す前に、 LoadLibrary()
を呼び出すことが可能になります。 ライブラリを記述している場合、 最初に P/Invoke 呼び出しが行われる前に、 そのライブラリが読み込まれることがないように、 GetModuleHandle()
を呼び出すことによってこれを防ぐことができます。
P/Invoke のトラブルシューティング
P/Invoke の呼び出しに失敗する原因は、 多くの場合、一部の型が不適切に定義されていることです。 以下に、いくつか共通の問題点を示します。
Long
とlong
は同じではありません。 C++ではlong
は 4 バイトの整数値ですが、 C# では 8 バイトの整数値です。- 文字列型が正しく設定されていません。
来月
来月は、C++ のラッピング関数について調査する予定です。
Eric Gunnerson は Visual C# チームのプログラム マネージャで、 C# デザイン チームのメンバーです。 そして『A Programmer's Introduction to C#』 (英語) の著者でもあります。 彼は、8 インチのフロッピー ディスクが何であるかを知っているぐらい昔からプログラミングを行っています。 さらに、かつては簡単にテープを装着できました。 余暇には、ペルシャ絨毯の上で精神を集中させてペルシャ猫ジャグリングの特訓に励んでいます。