マネージド コードからのネイティブ関数の呼び出し
共通言語ランタイムにはプラットフォーム呼び出しサービス (PInvoke: Platform Invocation Services) が用意されており、マネージド コードでネイティブなダイナミック リンク ライブラリ (DLL : Dynamic Link Library) の C スタイルの関数を呼び出すことができます。 COM とランタイムの相互運用と、"It Just Works (そのままで動く)" つまり IJW 機構のどちらにも、同じデータ マーシャリングが使用されています。
詳細については、以下を参照してください:
このセクションのサンプルで、PInvoke
の使い方を示します。 PInvoke
を使用すると、マーシャリング情報を手続き的なコードで書く代わりに属性として宣言できるため、データ マーシャリングを簡単にカスタマイズできます。
Note
マーシャリング ライブラリにより、最適化された方法でネイティブ環境とマネージド環境との間でデータ変換を行うことができるようになります。 マーシャリング ライブラリについて詳しくは、「C++ におけるマーシャリングの概要」をご覧ください。 マーシャリング ライブラリは関数には使用できず、データにしか使用できません。
PInvoke と DllImport 属性
次の例は、Visual C++ プログラムで PInvoke
を使用する方法を示しています。 ネイティブ関数 puts が msvcrt.dll で定義されています。 DllImport 属性は puts の宣言に使用されます。
// platform_invocation_services.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;
[DllImport("msvcrt", CharSet=CharSet::Ansi)]
extern "C" int puts(String ^);
int main() {
String ^ pStr = "Hello World!";
puts(pStr);
}
次の例は上のサンプルと同じものですが、IJW を使用しています。
// platform_invocation_services_2.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;
#include <stdio.h>
int main() {
String ^ pStr = "Hello World!";
char* pChars = (char*)Marshal::StringToHGlobalAnsi(pStr).ToPointer();
puts(pChars);
Marshal::FreeHGlobal((IntPtr)pChars);
}
IJW の長所
プログラムで使用するアンマネージ API のために
DLLImport
属性を宣言する必要がありません。 ヘッダー ファイルをインクルードし、インポート ライブラリをリンクするだけです。IJW 機構の方が多少高速です。たとえば、データ項目の固定またはコピーは開発者が明示的に行うため、IJW スタブでその必要性をチェックする必要はありません。
パフォーマンスの問題が明確になります。 この例では、Unicode 文字列から ANSI 文字列に変換しており、それに伴うメモリの割り当てと解放も行っています。 IJW でコードを書けば、
_putws
を呼び出し、PtrToStringChars
を使用した方がパフォーマンスが良くなることがわかります。同じデータを使用して複数のアンマネージ API を呼び出す場合は、データを 1 回マーシャリングし、マーシャリングしたコピーを渡す方が毎回マーシャリングするよりも効率的です。
IJW の短所
マーシャリングは、属性ではなくコードに明示的に指定する必要があります (多くの場合、属性には適切な既定値があります)。
マーシャリング コードがインラインであるため、アプリケーション ロジックのフローが途切れやすくなります。
API の明示的なマーシャリングでは、32 ビットから 64 ビットへの変換のため、
IntPtr
型が返されます。そのため、さらにToPointer
を呼び出す必要があります。
C++ によって公開された特定のメソッドの方が、ある程度複雑になりますが、わかりやすく効率的です。
アプリケーションで主にアンマネージ データ型を使用する場合、または .NET Framework API よりもアンマネージ API を多く呼び出す場合は、IJW 機能の使用をお勧めします。 アプリケーションの大部分でマネージド コードを使用し、ときどきアンマネージド API を呼び出す程度であれば、どちらを選択しても大きな違いはありません。
PInvoke と Windows API
PInvoke は、Windows の関数を呼び出すときに便利です。
次の例では、Visual C++ プログラムと Win32 API に属する MessageBox 関数との相互運用を示します。
// platform_invocation_services_4.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HWND;
[DllImport("user32", CharSet=CharSet::Ansi)]
extern "C" int MessageBox(HWND hWnd, String ^ pText, String ^ pCaption, unsigned int uType);
int main() {
String ^ pText = "Hello World! ";
String ^ pCaption = "PInvoke Test";
MessageBox(0, pText, pCaption, 0);
}
出力は「PInvoke Test」というタイトルのメッセージ ボックスで、「Hello World!」というテキストが表示されています。
PInvoke は、DLL から関数を検索するとき、マーシャリング情報も使用します。 user32.dll には実際は MessageBox 関数はありませんが、CharSet=CharSet::Ansi により、PInvoke は Unicode バージョンの MessageBoxW ではなく ANSI バージョンの MessageBoxA を使用できます。 一般に、Unicode バージョンのアンマネージ API を使用することをお勧めします。これは、.NET Framework の文字列オブジェクトのネイティブな形式である Unicode を ANSI に変換するオーバーヘッドがないためです。
PInvoke の使用が適さないケース
PInvoke は、DLL に属するすべての C スタイルの関数に適しているわけではありません。 たとえば、次のように宣言された mylib.dll の MakeSpecial 関数について考えてみます。
char * MakeSpecial(char * pszString);
Visual C++ アプリケーションで PInvoke を使用する場合、次のようなコードになります。
[DllImport("mylib")]
extern "C" String * MakeSpecial([MarshalAs(UnmanagedType::LPStr)] String ^);
ここで問題なのは、MakeSpecial によって返されるアンマネージ文字列のメモリを削除できないことです。 PInvoke によって呼び出された他の関数は、ユーザーによるメモリ解放の必要のない内部バッファーへのポインターを返します。 このケースでは、IJW 機能の方が適しているのは明らかです。
PInvoke の制限
パラメーターとして使用したものと同じ正確なポインターをネイティブ関数から返すことはできません。 PInvoke によってそれにマーシャリングされたポインターをネイティブ関数が返す場合、メモリが破損したり例外が発生したりすることがあります。
__declspec(dllexport)
char* fstringA(char* param) {
return param;
}
次の例はこの問題を示しています。プログラムが正確な出力を与えるように見える場合でも、その出力は既に解放されたメモリから行われています。
// platform_invocation_services_5.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
#include <limits.h>
ref struct MyPInvokeWrap {
public:
[ DllImport("user32.dll", EntryPoint = "CharLower", CharSet = CharSet::Ansi) ]
static String^ CharLower([In, Out] String ^);
};
int main() {
String ^ strout = "AabCc";
Console::WriteLine(strout);
strout = MyPInvokeWrap::CharLower(strout);
Console::WriteLine(strout);
}
引数のマーシャリング
PInvoke
を使用する場合、マネージド型と C++ のネイティブなプリミティブ型が同じフォームを持っていれば、マーシャリングの必要はありません。 たとえば、Int32 と int、または Double と double との間では、マーシャリングは不要です。
しかし、フォームが異なる場合はマーシャリングが必要です。 フォームが異なる型とは、char、string、struct などの型です。 次の表は、マーシャラーが各型に使用するマップを示します。
wtypes.h | Visual C++ | /clr を指定した Visual C++ | 共通言語ランタイム |
---|---|---|---|
HANDLE | void* | void* | IntPtr、UIntPtr |
BYTE | unsigned char | unsigned char | Byte |
SHORT | short | short | Int16 |
WORD | unsigned short | unsigned short | UInt16 |
INT | int | INT | Int32 |
UINT | unsigned int | unsigned int | UInt32 |
LONG | long | long | Int32 |
BOOL | long | bool | ブール型 |
DWORD | unsigned long | unsigned long | UInt32 |
ULONG | unsigned long | unsigned long | UInt32 |
CHAR | char | char | Char |
LPSTR | char * | String ^ [in], StringBuilder ^ [in, out] | String ^ [in], StringBuilder ^ [in, out] |
LPCSTR | const char* | String ^ | String |
LPWSTR | wchar_t * | String ^ [in], StringBuilder ^ [in, out] | String ^ [in], StringBuilder ^ [in, out] |
LPCWSTR | const wchar_t* | String ^ | String |
FLOAT | float | float | Single |
DOUBLE | double | double | Double |
マーシャラーは、ランタイム ヒープに割り当てられたメモリのアドレスをアンマネージ関数に渡す場合、メモリを自動的に固定します。 メモリの固定によって、割り当てられたメモリ ブロックをガベージ コレクターが圧縮時に移動することはなくなります。
このトピックの最初に示した例では、DllImport の CharSet パラメーターでマネージド型の String をマーシャリングする方法を示しています。この場合、マネージド型の String をネイティブ側で使用できるように ANSI 文字列にマーシャリングしています。
ネイティブ関数で使用する各引数のマーシャリング情報については、MarshalAs 属性で指定できます。 文字列 * 引数のマーシャリングには、BStr、ANSIBStr、TB (テラバイト)Str、LPStr、LPWStr、LPTStr という複数の選択肢があります。 既定値は LPStr です。
この例では、文字列を 2 バイトの Unicode 文字列 LPWStr としてマーシャリングしています。 出力されるのは、"Hello World!" の最初の文字です。 これは、マーシャリング後の文字列の第 2 バイトが null であり、puts は null を文字列の終わりを示すマーカーとして解釈するためです。
// platform_invocation_services_3.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;
[DllImport("msvcrt", EntryPoint="puts")]
extern "C" int puts([MarshalAs(UnmanagedType::LPWStr)] String ^);
int main() {
String ^ pStr = "Hello World!";
puts(pStr);
}
MarshalAs 属性は System::Runtime::InteropServices 名前空間にあります。 この属性は、配列などのほかのデータ型にも使用できます。
このトピックで既に述べたように、マーシャリング ライブラリにより、最適化された新しい方法でネイティブ環境とマネージド環境との間でデータ変換を行うことができます。 詳しくは、「C++ におけるマーシャリングの概要」をご覧ください。
パフォーマンスに関する考慮事項
PInvoke は、1 回の呼び出しで 10 ~ 30 個分の x86 命令のオーバーヘッドを要します。 この固定コストのほかに、マーシャリングによってオーバーヘッドが発生します。 マネージド コードとアンマネージド コードの間で同じ表現である blittable 型については、マーシャリングのコストはかかりません。 たとえば、int と Int32 との間の変換にコストは不要です。
パフォーマンスを上げるには、マーシャリングするデータが少ない PInvoke 呼び出しを何度も行うより、できるだけ多くのデータを少ない PInvoke 呼び出しでマーシャリングすることをお勧めします。