VC.NET ~ PInvoke、あるいはC++ Interop

前置き

ここでは、マネージ・コードからアンマネージ・コードを呼び出す方法について書きます。 関連するテーマ「マーシャリング」の記事も参照して下さい。

本文

Platform Invoke

Platform Invokeは、C#やVB、C++/CLIなどで書かれたマネージ・コードから、Win32API関数やアンマネージなdllの関数を呼ぶための仕組みです。P/InvokeとかPInvokeと呼ばれることも多いようです。

PInvokeでは、呼び出したいアンマネージ関数に関する以下のような情報を、アトリビュート表記を使って記述します。

  • どのdllで実装されているか
  • 関数の名前
  • 引数とreturn値の型
  • 引数とreturn値の型のマーシャリング方法

例えば、Win32API関数のGetSystemMetrics()に関する情報は以下のように記述します。

using namespace System::Runtime::InteropServices;

namespace NMI {
  [DllImport("User32.dll")]            // (A)
  int GetSystemMetrics(int);           // (B)
  const int SM_CXSCREEN = 0;
}

(A)でdllを、(B)で関数名と引数/return値の型を指定しています。namespaceと定数宣言は任意です。

次に、GetSystemMetrics()を呼んでみましょう。

Console::WriteLine(NMI::GetSystemMetrics(NMI::SM_CXSCREEN));         // 1280

このように、引数やreturn値が基本型の場合は、マーシャリングのことを気にする必要はありません。

文字列を使う関数

MessageBox()のように、文字列を引数に取る関数の場合は以下のようになります。

namespace NMI {
  [DllImport("User32.dll", CharSet=CharSet::Unicode)]      // (A)
  int MessageBox(void*, String^, String^, unsigned int);
  const int MB_OK = 0;

  [DllImport("User32.dll", EntryPoint="MessageBox", CharSet=CharSet::Ansi)]    // (B)
  int MessageBoxAnsi(void*, String^, String^, unsigned int);                   // (B*)

  [DllImport("NativeDll.dll")]                             // (C)
  extern "C" void NativeFuncStr([MarshalAs(UnmanagedType::LPStr)]String^);
}

(A)では、CharSetフィールドにUnicodeを指定することにより、Stringがwchar_tポインタへ自動的にマーシャリングされます。またMessageBox()の実体には、マルチバイト文字版のMessageBoxA()とワイド文字版のMessageBoxW()がありますが、ここではMessageBoxW()が使用されます。

(B)では、EntryPointフィールドにより関数名を指定しています。さらにCharSetをAnsiにしました。この結果、マネージ側でMessageBoxAnsi()を呼ぶと、Stringがcharポインタにマーシャリングされ、アンマネージ側のMessageBoxA()が呼ばれます。

(C)では、関数全体ではなく、個々の引数ごとにマーシャリング方法を指定しています。この例では、Stringがcharポインタへマーシャリングされます。

なおNativeDll.dllは、マルチバイト文字セット(/D "_MBCS")でビルドされており、NativeFuncStr()の実装は以下の通りです。

NATIVEDLL_API void NativeFuncStr(char* str)
{
  printf("%s\n", str);
}

それぞれの関数をPInvokeで呼んでみます。

String^ msg = L"メッセージ";
String^ title = L"タイトル";
NMI::MessageBox( 0, msg, title, NMI::MB_OK );
NMI::MessageBoxAnsi( 0, msg, title, NMI::MB_OK );
NMI::NativeFuncStr(gcnew String(L"漢字"));                 // 漢字

配列を使う関数

blittableな型の配列であれば、LPArrayを指定すると自動的にアンマネージ用の配列へマーシャリングされます。

namespace NMI {
  [DllImport("NativeDll.dll")]
  extern "C" void NativeFuncArray([MarshalAs(UnmanagedType::LPArray)]array<Int32>^);
}

NativeFuncArray()の実装は、こんな感じです。

NATIVEDLL_API void NativeFuncArray(int* ar8)
{
  for ( int i = 0; i < 8; i++ ) printf("%d", ar8[i]);
  printf("\n");
}

呼びます。

NMI::NativeFuncArray(gcnew array<Int32>{ 1, 2, 3, 4, 5, 6, 7, 8 });  // 12345678

構造体を使う関数

以下のようなアンマネージ関数を考えてみましょう。構造体メンバのアライメントは4バイト(/Zp4)でビルドします。

struct NativeData {
  unsigned char a;
  unsigned int b;
  unsigned char c;
  unsigned long* d;
};

NATIVEDLL_API void NativeFuncData(NativeData data)
{
  printf("%d, %d, %d, %08x\n", data.a, data.b, data.c, *(data.d));
}

このような関数をマネージ側から呼ぶには、まず、NativeDataと等価な構造体をマネージ側にも宣言する必要があります。

namespace NMI {
  [StructLayout(LayoutKind::Sequential, Pack=4)]
  value struct NativeData {
    Byte a;
    UInt32 b;
    Byte c;
    IntPtr d;
  };

  [DllImport("NativeDll.dll")]
  extern "C" void NativeFuncData(NativeData);
}

StructLayoutアトリビュートにより、各フィールドが順に並び(LayoutKind::Sequential)、アライメントに4バイト(Pack=4)を指定します。また、アンマネージ側のフィールドd(unsigned longポインタ)に相当する部分にはIntPtrを使っています。

呼ぶときの手順も面倒です。

NMI::NativeData data;
data.a = 8;
data.b = 8000;
data.c = 9;
data.d = Marshal::AllocCoTaskMem(Marshal::SizeOf(unsigned long::typeid) * 1);
Marshal::StructureToPtr(Convert::ToUInt32(0xFEDC1234), data.d, false);
NMI::NativeFuncData(data);                      // 8, 8000, 9, fedc1234
Marshal::FreeCoTaskMem(data.d);

まず、フィールドd用のメモリ領域をMarshal::AllocCoTaskMem()で確保しています。メモリのサイズはMarshal::SizeOf()で計算します。次に、確保した領域へMarshal::StructureToPtr()で値をコピーします。もしフィールドdが配列の場合は、コピー先(StructureToPtrの第2引数)をずらしながら要素数分コピーします。

AllocCoTaskMem()した領域が不要になったら、Marshal::FreeCoTaskMem()で解放します。

もしNativeFuncData()が、NativeDataを返すような関数だった場合は、Marshal::PtrToStructure()で値を取り出して下さい。

C++ Interop

C++/CLIでは、PInvokeとは別に、もう1つ、アンマネージコードを呼ぶ仕組みが提供されています。それがC++ Interopです(「暗黙のPInvoke」とも呼ばれるようです)。名前が示す通りC++/CLI特有の機能なので、C#やVBでは使えません。

C++ Interopでは、アンマネージコードを呼ぶために、わざわざdll名や関数プロトタイプを教える必要はありません。従来の通り、ヘッダを#includeし、関数を呼び、libをリンクして下さい。C++/CLIと言えどもC++の端くれなので、普通に呼べます。

ただし、基本型以外のデータをやり取りする場合は、自力でマーシャリングする必要があります。自力のマーシャリングに関しては、別記事にまとめてあります。

Last modified:2012/03/19 15:38:54
Keyword(s):
References:[.NETアプリ開発] [VC.NET ~ プロジェクト管理の落とし穴]
This page is frozen.