P/Invoke の宣言を支援するための dumpbin.exe の使用

Chris Tacke
Applied Data System

March 2003

対象 :
    Microsoft® .NET Compact Framework 1.0
    Microsoft® Visual Studio® .NET 2003

概要 : Microsoft .NET Compact Framework ベースのアプリケーションで、P/Invoke の宣言を支援するための dumpbin.exe の使用方法を調査します。

目次

はじめに
P/Invoke
dumpbin.exe
DLL からの関数の公開
関数パラメータ
まとめ

はじめに

.NET Compact Framework は、 Pocket PC 2000、2002、および Microsoft® Windows® CE .NET 4.1 を搭載したデバイスを対象とし、完全な .NET Framework のサブセットとして開発されました。 開発者は、.NET Compact Framework を VS .NET のスマート デバイス プロジェクト サポートと組み合わせて使用することにより、完全な .NET Framework のマネージ コードと同様の、共通言語ランタイムで実行される Visual Basic や C# のマネージ コードを記述できるようになります。

ただし、.NET Compact Framework は .NET Framework のサブセットなので、ユーザー入力、メッセージング、および Microsoft® SQL™ Server 2000 Windows CE Edition 2.0 を処理するいくつかの .NET Compact Framework 固有の型と、名前空間全体にまたがる型のおよそ 25% しかサポートしていません。 このことは開発者にとって、オペレーティング システム (Windows CE) API にドロップ ダウンする方法でのみアクセスできる機能が一部あることを意味します。

幸運なことに、(デスクトップ フレームワークによく似た) .NET Compact Framework は、 Platform Invoke と呼ばれる優れた "スイス アーミー ナイフ" (多目的機能) を開発者に提供します。 マネージ コードはこのサービスにより、DLL に存在するアンマネージ関数を呼び出すことができます。

P/Invoke

Platform Invoke は実際には新しいものではありません。 名前が新しいだけなので、おそらくこの概念には馴染みがあるでしょう。 VB 開発者には、この概念は Declare として知られ、C 開発者には dllimport として知られています。 簡単に言うと、 P/Invoke は、ダイナミック リンク ライブラリ (DLL) から公にエクスポートされる関数を呼び出すための方法です。

.NET Compact Framework は非常に軽量なので、開発者は折に触れ P/Invoke 外部関数が必要になることがあります。 VB 開発者の場合、API ビューアやかなり包括的なオンライン アーカイブが存在するので、多くの場合、Declare ステートメントを実際に記述する方法についてあまり理解していなくても、外部関数を呼び出せました。

.NET Compact Framework には API ビューアのようなツールがありません。 その一方で、インターネット上に存在する数多くのサンプルやリソースが急激に増加しています。 ただし、必要な P/Invoke 関数宣言すべてを提供する信頼性の高いコード アーカイブはまだありません。 VB 開発者と eVB 開発者のみなさん、これは、C 開発者が信頼してきた dumpbin.exe というツールをよく知るための良い機会です。

dumpbin.exe

Common Object File Format (COFF) バイナリ ファイル (DLL、LIB、EXE など) は、それらのファイルが公開するすべての関数を含めて、多くの役立つヘッダー情報を提供します。 Microsoft は、 Visual Studio .NET 2003 を含めた、ほとんどすべての開発環境で、簡単にこの情報を抽出できるコマンドライン ツールを提供します。 このツールが dumpbin.exe です。

今説明したように、dumpbin.exe はコマンドライン ツールなので、これを使用するには [スタート] ボタン、[ファイル名を指定して実行] を順にクリックして「cmd.exe」と入力して、コマンド ウィンドウを開く必要があります。 dumpbin.exe がシステム パスには存在しない場合があるので、入力の手間を最小限にするために、dumpbin.exe のコマンドを、dumpbin.exe が存在するディレクトリから実行するのが最も簡単な方法です。

ヒント    Visual Studio 2003 を使用している場合、dumpbin.exe は既定で C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\bin\ にインストールされます。 私が使用しているシステムでは、単純に dumpbin.exe を実行すると、mspdb71.dll が見つからなかったためにエラーになりました。 この現象を修正し、かつ、システム パスを変更しないようにするために、 dumpbin.exe を mspdb71.dll が存在する C:\Program Files\Microsoft Visual Studio .NET 2003.0\Common7\IDE\ ディレクトリに単純にコピーし、そこから dumpbin.exe を実行しました。

dumbin.exe では数多くのコマンドライン スイッチを使用できますが、最も興味深いコマンドライン スイッチは /EXPORTS です。 これは、COFF が公開するすべての関数の名前を表示します。 このコマンドライン スイッチの使用例を見てみましょう。

CoreDLL.dll は Windows CE カーネルを含んでおり、アプリケーションで使用できる多くの Win32 関数を公開します。 eVC と Pocket PC 2002 SDK をインストールした場合は、 Dumbin.exe を使用して CoreDLL.dll に対して次のコマンドを実行すると、これらの関数を参照できます。

Dumbin.exe /EXPORTS "C:\Windows CE Tools\wce300\Pocket PC 2002\lib\arm\coredll.lib"

ご想像どおり、このコマンドを実行すると非常に長いダンプが作成されます。 以下はいくつかの共通関数を示すために余分な部分を大幅に取り除いたダンプです。 これらの共通関数についてさらに詳しく検討していきます。

Microsoft (R) COFF/PE Dumper Version 7.10.2292
Copyright (C) Microsoft Corporation.  All rights reserved.
Dump of file C:\Windows CE Tools\wce300\hpc2000\lib\arm\coredll.lib
File Type: LIBRARY
     Exports
       ordinal    name
            23    GetLocalTime
           257    GetWindowTextW
            33    LocalAlloc
            36    LocalFree

注意    COM 標準では、COM DLL が DllCanUnloadNowDllGetClassObjectDllRegisterServerDllUnregisterServer を公開する必要があります。 一般的には、COM DLL はこれらの関数以外は公開しません。 つまり、dumpbin.exe では COM オブジェクトや COM メソッドの情報を取得できません。

DLL からの関数の公開

ダンプの先頭に、ある程度判読可能な名前の関数が存在することにお気付きかもしれません。 それらは疑問符かアンダースコアで始まり、その後に文字列が付加されている場合があります。 これらは装飾名です (多くの場合、マングル名と呼ばれます)。

既定では、コンパイラは名前を装飾しますが、これは内部的な名前の参照方法です。 ただし装飾名は、コーディングするさいそれほど判読しやすくも便利でもないので、装飾されない関数名を公開するリンカを取得する方法がいくつか存在します。

次の関数が存在すると仮定しましょう。

int foo(int a) {...}

そして、次のような dllexport 記憶クラス属性を追加して公に公開される関数を作成します。

int __declspec(dllexport)
foo(int a) {...}

この関数は foo() を、マングル名 ?foo@@YAHH@Z で公開します。 これを装飾せずに公開する方法は 2 つあります。 最初の方法は、関数の宣言時に extern "C" 宣言を使用する方法です。 その場合、宣言は以下のようになります。

extern "C" int __declspec(dllexport)
foo(int a) {...}

もう 1 つの方法として、公開される関数を記述するプロジェクトに DEF ファイルを追加できます。 .DEF 拡張子のファイル (これを MyDef.DEF とします) をプロジェクトに単純に追加し、次に以下のテキストをファイルに追加します。

LIBRARY      MyDLL
EXPORTS
   Foo

次に、リンカ オプションをプロジェクトに追加します。

/def:"mydef.def"

装飾名と装飾名の規則に関する詳細については、 MSDN の記事「装飾名」を参照してください。

関数パラメータ

既にお気付きかもしれませんが、 dumpbin.exe を使用すると公開された関数の名前はわかりますが、関数のパラメータ名を知ることはできません。 パラメータの型さえもわかりません。これらについては、ライブラリのヘッダー ファイルを必要とするでしょう。

ヘッダー ファイルはライブラリの発行者か作成者から入手できるはずです。 ただし、名前が DLL ファイルや LIB ファイルと一致しないこともあります。 たとえば、coredll.lib のダンプで見た GetWindowTextWwinuser.h で定義されています。

多くの場合、P/Invoke の呼び出し時にパラメータとして使用するデータ型を判断することは、非常に単純です。 ただし、使用するデータ型を厳密に判断することが困難な場合もあります。 実際に、複数の方法で呼び出せる場合がよくあります。 以下の 2 つの短い例を見てみましょう。

まず非常に単純な API である SetSystemTime を見てみましょう。 SetSystemTime は 単一の入力パラメータか SYSTEMTIME 構造体を受け取ります。 受け取ったものが構造体の場合、そのメンバはすべて固定長なので、CF は何の問題もなくマーシャリングできます。 コードで SYSTEMTIME 構造体を単純に宣言するだけで済みます。

public struct SYSTEMTIME
{
   public UInt16 Year ;
   public UInt16 Month ;
   public UInt16 DayOfWeek ;
   public UInt16 Day ;
   public UInt16 Hour ;
   public UInt16 Minute ;
   public UInt16 Second ;
   public UInt16 MilliSecond ;
}

P/Invoke 関数を宣言します。

[DllImport("Coredll.dll") ", EntryPoint="SetSystemTime"]
private static extern bool SetSystemTime(ref SystemTime st);

次に行う関数の呼び出しは以下のように簡単です。

SYSTEMTIME st = new SYSTEMTIME(2003, 1, 0, 15, 18, 19, 00, 0);
SetSystemTime(ref st);

実際のアプリケーションでは、SYSTEMTIME 構造体の値を (たとえば、minutes < 60 などのように) 後で有効にすることができるパブリック メソッドで、実際の P/Invoke SetSystemTime 呼び出しをラップする方がおそらく賢明でしょうが、読者の課題としてここではそのまま使用します。

少し複雑な API もありますが、先ほどお話ししたように API の呼び出しには複数の方法があります。 それらの方法では、通常、可変長のデータ パラメータ (バッファ ポインタ) または可変長のデータ要素を含む構造体を持つことになります。

後者の場合 (可変長のデータを含む構造体) は、.NET Compact Framework マーシャラの制約が原因で、かなり複雑になります。 それらを、fixed ステートメントと unsafe ステートメントを使用して処理する方法を説明することは、この資料の範囲を超えています。 GetWindowText API を使用するもっと簡単な例を検討しましょう。

GetWindowText の定義は、パラメータの 1 つとして文字列バッファへのポインタを受け取ります。 バッファが既に割り当てられているかどうか、さらにコピーされるデータを保持できるほど大きいかどうかは呼び出し元によって異なります。 このバッファを 3 とおりの方法で処理できます。
1) StringBuilder クラスとして処理できます。StringBuilder クラスは文字列バッファを囲む .NET ラッパーです。
2) バイトの配列として処理できます。
3) IntPtr として処理できます。IntPtr はメモリ ハンドルを囲む .NET ラッパーです。
どれを使用するかは個人の好みになりますが、それぞれの方法には固有の長所と短所があります。

StringBuilder がおそらく最も簡単に使用できる形式をしており、開発者がシンプルな .NET Compact Framework のコーディングをする練習に合っています。 欠点として、使用者に見えない部分でさまざまなことが起こるので、最も効果的な処理方法というわけではありません。 どのようなコードなのかを見てみましょう。

最初は P/Invoke 宣言です。

[DllImport("Coredll.dll") ", EntryPoint="GetWindowTextW"]
private static extern int GetWindowText(
                                          int hWnd
                                          StringBuilder lpString
                                          int nMaxCount);

このように宣言した場合、以下のようにして、単純に StringBuilder オブジェクトを作成して適切にサイズを確認し、 P/Invoke メソッドに StringBuilder オブジェクトを渡す必要があります。

StringBuilder sb = new StringBuilder(bufferSize);
GetClassWindowText(hwnd, sb, bufferSize);
MessageBox.Show(sb.ToString());

バイト配列を使用する方法は、マーシャリングのオーバーヘッドに限っては、より効果的な API の呼び出し方法です。 しかし、結局、後から文字列を取得する処理を行う必要があります。 ただし、バイト配列を受け取ってさらに処理する場合はうまく行きます。

P/Invoke 宣言は、2 つ目の入力パラメータ部分のみが変わるので、以下のようになります。

[DllImport("Coredll.dll") ", EntryPoint="GetWindowTextW"]
private static extern int GetWindowText(
                                          int hWnd
                                          byte[] lpString
                                          int nMaxCount);

また、以下のように呼び出されます。

byte[] buffer = new byte[bufferSize];
int len = GetWindowText(hwnd, buffer, buffersize);
string windowName = Encoding.Unicode.GetString(buffer, 0, len);
MessageBox.Show(windowName);

GetWindowText の場合、パフォーマンスが問題となることはほとんど考えられないのですが、他の API を使用する場合は問題になることがあります。 その場合は、IntPtr を使用して API を呼び出すと、かなりのオーバーヘッドを削減できます。 ただし批判的な見方をすると、メモリ バッファを手動で割り当てたり解放したりする必要があります。 これは完全に .NET Compact Framework のメモリ管理を使用しない方法なので、注意深く使用しないと、コードが大きくなり、メモリ リークが発生します。

どのプロジェクトでも、そのリスクとパフォーマンスを比較検討する必要があります。 ここでも、P/Invoke 宣言は 2 つ目の入力パラメータ部分のみが変わるので、以下のようになります。

[DllImport("Coredll.dll") ", EntryPoint="GetWindowTextW"]
private static extern int GetWindowText(
                                          int hWnd
                                          IntPtr lpString
                                          int nMaxCount);

先ほど述べたように、メモリの割り当てと解放は手動で処理する必要があります。 これは、LocalAlloc API と LocalFree API を使用して行います。 これらの API も P/Invoke で使用します。 各宣言は以下のようになります。

[ DllImport("coredll.dll")]
public static extern IntPtr LocalAlloc(uint flags, uint cb);
[ DllImport("coredll.dll")]
public static extern IntPtr LocalFree(IntPtr p);

実装は以下のようになります。

IntPtr p = LocalAlloc(0x40, bufferSize);
GetClassName(hwnd, p, bufferSize);
string windowName = Marshal.PtrToStringUni(p);
LocalFree(p);
MessageBox.Show(windowName);

まとめ

.NET Compact Framework 開発者は、DLL 関数をマネージ コードから直接呼び出す必要があることに気付くことがあります。 dumpbin.exe のようなツールの活用、およびヘッダー ファイルの調査により、開発者は必要な P/Invoke を迅速かつ簡単に系統立てて表すことができます。