VC.NET ~ interior_ptrとpin_ptrとIntPtr

前置き

C++/CLIでも、ポインタを使いたくなる場面はあります。また、ネイティブの(アンマネージの)関数を呼び出すときなどは、ポインタを使わざるをえません。そこで、C++/CLIのポインタについてまとめます。

本文

3種類のポインタ

  • interior_ptr
  • pin_ptr
  • IntPtr

interior_ptrは、マネージなオブジェクトのアドレスが欲しいときに使います。このアドレスはGC透過とでも言いましょうか、ポイント先のオブジェクトがGCで再配置されても、ちゃんと追跡してくれるのが特徴です。なので、再配置を気にせずに、ポインタ演算やポインタ経由でのメモリへのアクセスが可能です。

pin_ptrは、interior_ptrと似てますが、GCによる再配置を禁止する点が違います。pin、つまり固定するということですね。

最後のIntPtrは、ネイティブなアドレスやハンドル(HANDLEやHWNDなど)をマネージな世界で扱うためのクラスです。ラッパと考えて構いません。value classと定義されているので、value typeの仲間です(value typeについては別の記事を参照)。

サンプルコード

array<unsigned char>^ ar = { 1, 2, 3, 4 };
interior_ptr<unsigned char> p = &ar[0];           // arや&arじゃダメ。(A)
*p = 0;
p++;
*p = 8;                                           // (B)
for each ( unsigned char b in ar ) Console::Write(b);
Console::WriteLine("");                           // 0834

unsigned char arr[] = { 9, 8, 7, 6 };
pin_ptr<unsigned char> pp = &ar[0];
memcpy(pp, arr, 4);
pp = nullptr;                                     // (C)
for each ( unsigned char b in ar ) Console::Write(b);
Console::WriteLine("");                           // 9876

IntPtr ip(arr);
System::Runtime::InteropServices::Marshal::Copy(ip, ar, 0, 4);
for each ( unsigned char b in ar ) Console::Write(b);
Console::WriteLine("");                           // 9876

pはinterior_ptrなので、もし(A)から(B)の間にGCが配列arを再配置したとしても、コードは意図通りに動きます。意図通りに動くようなMSIL(CIL)に、C++/CLIコンパイラが翻訳してくれるわけです。

一方、ネイティブの関数にそんなことは期待できないので、memcpy()にはpin_ptrを渡す必要があります。(C)でppを未定義にするまで、GCによる配列arの再配置は抑止されます。

IntPtrは、ネイティブのアドレス値やハンドル値で初期化します。マーシャリングで活躍するMarshalクラスには、IntPtrを引数に取るメソッドがいろいろ定義されています。また、HWNDを内包するクラス(Buttonとか)が、HWNDをプロパティとして公開するときなどにも、IntPtrが使われます。

ちなみにIntPtrのIntは、Internalではなく、IntegerのIntです。対称性を保つためにUIntPtrというクラスも用意されていますが、あまり使うことはないようです。

interior_ptrとpin_ptrについて、もう1つサンプルコードを挙げます。

interior_ptr<int> ip;                            // warning C4700
if ( ip == nullptr ) Console::WriteLine("yes");  // yes

pin_ptr<int> ipp;                                // warning C4700
Console::WriteLine(ipp ? "not null" : "null");   // null

interior_ptrもpin_ptrも、未初期化の場合はnullptrとして扱われます。とは言え、コンパイル時にwarningが出てしまうので初期化した方が良いでしょう。また、ネイティブのポインタと同様、条件式の中で使うことができます(暗黙でboolにキャストされるようです)。

Blittableとは?

上記のサンプルコードで、マネージな配列arの内容をmemcpy()で変更していますが、これは、ar[]がblittableだからこそ可能な技です。blittableとは、マネージなデータで、メモリ上でのデータのレイアウトがネイティブな型のレイアウトと同じものを指します。blitと聞くと、BitBlt()というWin32API関数を連想する人もいると思いますが、まさしく、そのbit blitから来た言葉のようです。BitBlt(SRCCOPY)可能ってことですね。余談ですが、BitBlt()をついつい「ビットビルト」と読んでしまうのは私だけでしょうか。

例えば、ByteやInt32、Double、IntPtrなどはblittableです。また、blittableなデータの1次元配列もblittableです。一方、StringやBoolean、Charといったクラスはblittableではありません。interior_ptrやpin_ptr経由でオブジェクトにアクセスするときは、そのクラスがblittableかどうか意識しておきましょう。

ちなみに、"bittable"じゃないです。エル(l)が入ります。

Stringの走査

Stringはblittableではないのですが、interior_ptrやpin_ptrを使って、String内の各文字を走査することは可能です。それには、vcclr.hで定義されている関数PtrToStringChars()を使います。なぜStringクラスやMarshalクラスのメソッドになってないのか不可解ですが、恐らくC++/CLIの前身であるManaged C++の時代の名残でしょう。

#include <vcclr.h>

--------

String^ s = "Ruby, Erlang, Haskell";
Console::WriteLine(s[3]);                     // y
interior_ptr<const Char> ps = PtrToStringChars(s);
Console::WriteLine(ps[3]);                    // y
for ( int i = 0; i < 10; i++ ) Console::Write(*ps++);
Console::WriteLine("");                       // Ruby, Erla

PtrToStringChars(s)は、sの先頭文字を指すconst Charポインタ(const wchar_tポインタ)を返します。別に、関数内部で文字列の複製を作っているわけではないので、sの内部を指すポインタになります。ポイント先がGCで再配置される可能性があるので、ポインタをネイティブ関数へ渡すときはpin_ptrで固定する必要があります。

これって、for eachや、[]によるインデックス指定で走査するのに比べると速いんでしょうね。Stringクラス内部のデータ構造に依存してしまうのが悲しいところです。

Last modified:2012/03/06 10:43:34
Keyword(s):
References:[.NETアプリ開発] [VC.NET ~ マーシャリング] [VC.NET ~ C++/CLIマイナスC++]
This page is frozen.