VC.NET ~ マーシャリング

前置き

C++ Interopを使ってマネージなコードからアンマネージな関数を呼ぶ場合、マネージ特有の型のデータをアンマネージ向けに変換する必要があります。逆にアンマネージから返ってきたデータをマネージな型に変換しておいた方がマネージ側では使いやすいでしょう。こういった変換処理をマーシャリングと呼びます。

マーシャリングには、System::Runtime::InteropServices::Marshalクラスが大活躍します。ここでは、様々な型のマーシャリング例を挙げます。

本文

String⇔NULL終端文字列

String^ fname = "foo.txt";

IntPtr ip = Marshal::StringToHGlobalAnsi(fname);
const char* p = static_cast<const char*>(ip.ToPointer());
Console::WriteLine(strlen(p));                             // 7
Console::WriteLine(Marshal::PtrToStringAnsi(IntPtr(const_cast<char*>(p))));
                                                           // foo.txt
Marshal::FreeHGlobal(ip);

ip = Marshal::StringToHGlobalUni(fname);
const wchar_t* wp = static_cast<const wchar_t*>(ip.ToPointer());
Console::WriteLine(wcslen(wp));                            // 7
Console::WriteLine(Marshal::PtrToStringUni(IntPtr(const_cast<wchar_t*>(wp))));
                                                           // foo.txt
Marshal::FreeHGlobal(ip);

ip = Marshal::StringToHGlobalAuto(fname);
p = static_cast<const char*>(ip.ToPointer());
Console::WriteLine(strlen(p));                             // 1
wp = static_cast<const wchar_t*>(ip.ToPointer());
Console::WriteLine(wcslen(wp));                            // 7
Console::WriteLine(Marshal::PtrToStringAuto(IntPtr(const_cast<wchar_t*>(wp))));
                                                           // foo.txt
Marshal::FreeHGlobal(ip);
  • StringToHGlobalXxx()は、Stringの複製をネイティブ・ヒープに作って、そのポインタを返します
  • 文字セットに応じてAnsiとUniを使い分けましょう
  • Autoの場合、コンパイルオプションで設定された文字セットに従います
  • プロジェクトの[プロパティ]→[構成プロパティ]→[全般]→[文字セット]
  • 文字セットを「Unicode文字セット」に設定した場合は、上記のようにstrlen()が1を返してしまいます
  • StringToHGlobalXxx()が作った複製は、FreeHGlobal()で明示的に解放する必要があります
  • PtrToStringXxx()は、NULL終端文字列の複製をStringオブジェクトとして返します

関数ポインタ

アンマネージ側が関数ポインタを受け取るとします。

typedef void (__stdcall *WikiCB)(const char* str);

---

void wiki_enum_langs(WikiCB cb)
{
  cb("Ruby");   // コールバック
  cb("Perl");   // コールバック
  cb("R");      // コールバック
}

関数ポインタの呼び出し規約は__stdcallとします(マネージ側の都合です)。

マネージ側からwiki_enum_langs()を呼ぶには、デリゲートを関数ポインタWikiCBに化けさせます。

delegate void WikiDele(const char* str);

ref class Foo {
public:
  Foo() {}
  void foo(const char* str) {
    Console::WriteLine(Marshal::PtrToStringAnsi(IntPtr(const_cast<char*>(str))));
  }
};

void fp()
{
  Foo^ f = gcnew Foo();
  WikiDele^ d = gcnew WikiDele(f, &Foo::foo);              // (A)

  IntPtr ip = Marshal::GetFunctionPointerForDelegate(d);
  WikiCB cb = static_cast<WikiCB>(ip.ToPointer());

  wiki_enum_langs(cb);       // Ruby
                             // Perl
                             // R
}

MSDNによると、(A)で生成したデリゲートはGCで回収/再配置される可能性があるそうです。再配置はいいけど、回収されるのはマズイです。こういった状況、つまり、オブジェクトがマネージ側では参照されてないけどアンマネージ側では参照されている場合、そのオブジェクトがGCされるのを回避するには、GCHandle::Alloc()を使います。

  GCHandle gch = GCHandle::Alloc(d);

  ...

  gch.Free();

Alloc()してからFree()するまでの間は、GCによる回収が抑止されます。

ただ個人的には、(少なくとも上記のコードなら)こんなことする必要は無いと思っています。dが生きてる間はGCされる心配は無いでしょう。もし、fp()がreturnした後もアンマネージ側がcbを保持するのなら、GCをケアする必要がありますが、その場合でも、dをpersistentな場所に保持しておけばいいだけのことでは?

配列

マネージ配列をアンマネージに渡すには、Stringのときのように複製を作って渡します。ただし、複製は自力で作ります。

array<int>^ ia = { 1, 2, 3 };
int* a = new int[ia->Length];
Marshal::Copy(ia, 0, IntPtr(a), ia->Length);

wiki_plus1(a, ia->Length);                 // 各要素に1を足す。

Marshal::Copy(IntPtr(a), ia, 0, ia->Length);

for each ( int i in ia ) Console::Write(i);
Console::WriteLine("");                    // 234

Marshal::Copy()の引数についてはMSDNを参照して下さい。16通りのオーバーロードがあります。ちなみに、array<int>はblittableなので、pin_ptrとmemcpy()を使っても複製を作れます。詳しくは別記事を参照して下さい。

Last modified:2012/03/07 23:43:28
Keyword(s):
References:[.NETアプリ開発] [VC.NET ~ PInvoke、あるいはC++ Interop]
This page is frozen.