2007年10月21日

String^ からポインタへ

今回は珍しく C++/CLI のお話。なお、C# と混ざると型名がややこしくなるので、今回はマネージドの型については CLR の汎用型名で統一します。

今ふと思い立ってとある C 言語のライブラリを C++/CLI でラップしようと試みているところなのですが、そこでふと気になったのが多分 C++/CLI 使う人たちが一度は立ち止まる問題である、String^ から wchar_t* への変換のこと。

一般的には、ToCharArray の先頭要素のアドレスを pin_ptr<wchar_t> で受けたり、Marshal::StringToHGlobalUni でポインタにしたりする、と言う風な解説がなされますが、これらはメモリのコピーが発生するためどうしてもパフォーマンスに影響が出てきます。後者なんか FreeHGlobal の呼び出し責任も出てきますし。

実のところ、この点に関してだけは C# の方が優れた解決を提供しています。fixed を使えば考える必要なく String を Char* にしてくれますから。同じ動作を C++/CLI で書くとしたら、pin_ptr<Char> pointer = &original[0]; ってことになるんでしょうが、残念ながらコンパイラはこの記述を認めてくれません。String の [] はプロパティ呼び出しであって配列の添え字じゃないですからねぇ。

C# でポインタに変換したい場合、私はよく GCHandle を使用します(まあ「よく」といってもそもそも使う機会はほとんどないですが)。これなら .NET に用意されている機構ですから言語に関係なく使用できますね。ただ、ネックは必ず GCHandle.Free を呼び出す必要があることです。

try-finally 構文があるとは言え、やはりどうせなら pin_ptr みたいに自動変数でどうにかしていただきたい。そこで再び C# の fixed 構文に目を向けて見ます。そもそもこいつは一体どうやって実装されているんでしょう? 結構この String に対する fixed は変態的で、String が変更不可オブジェクトとされているにも関わらず Char* の任意の位置の値をがりがり書き換えることができます。もちろん、元の String にもそれが反映されます。気持ち悪いですね。

気持ち悪いどころか、同じインターンプールを参照している文字列があれば当然のようにその文字列も変更されてしまいます。発見の著しく困難なバグの種になるでしょうから、Char* への変更はやめておいたほうがいいでしょう。

コードを追いかけるだけではどうにもならないので、ildasm を使って MSIL でどう実現されているのかを調べてみます。

コード
unsafe void Sample1(String text) {
    fixed (Char* ptr = text) {
        ptr[0] = 'a';
    }
}
MSIL(抜粋整形)
.locals init (char* V_0,
              string pinned V_1)
IL_0000:  ldarg.0
IL_0001:  stloc.1
IL_0002:  ldloc.1
IL_0003:  conv.i
IL_0004:  dup
IL_0005:  brfalse.s  IL_000d
IL_0007:  call       int32 [mscorlib]
                     System.Runtime.CompilerServices.RuntimeHelpers
                         ::get_OffsetToStringData()
IL_000c:  add
IL_000d:  stloc.0
IL_000e:  ldloc.0
IL_000f:  ldc.i4.s   97
IL_0011:  stind.i2
IL_0012:  ldnull
IL_0013:  stloc.1
IL_0014:  ret

まず目を引くのがローカル変数宣言部分の Char* 型変数についている pinned キーワードです。どうやらこのキーワードを持つ変数に代入されたオブジェクトは自動的に固定されるようです。ちなみに、System.Reflection.Emit.ILGenerator クラスの DeclareLocal メソッドには第二引数に Boolean を取るオーバーロードが存在し、これで pinned 指定をするかどうかを決定できます。

しかしこのオーバーロードは .NET 2.0 以降から追加されたもののようで、一体 .NET 1.1 ではどうすれば実現できたんでしょうか? やっぱ無理だったのかな。

IL コードではあまり悩む様子もなく、引数の String をロードしてまず pinned なローカル String 変数に代入して、それ(String への参照ですね)をポインタ型に変換しています。次に、唐突の感がありますが System.Runtime.CompilerServices.RuntimeHelpers.OffsetToStringData の値を加算し、最終的に Char* に変換しています。このプロパティは、名前どおり String オブジェクト内の実データを格納している部分へのオフセットと言うことなのでしょう。

ちなみに、conv.i は native int に変換するそうです(参考:MSDN2ライブラリ/OpCodes.Conv_I フィールド )。int なんだから +1 が 4/8 バイトになるんじゃないかと思ってしまいますが、+1 で 1 バイト進むだけみたいです。

目指すべき形は見えてきました。その前に、C++/CLI で似たようなコードで感覚をつかんでみましょう。pin_ptr の型引数(って言うのかしらん?)にはハンドル型も渡すことができます。

pin_ptr<String^> pointer = &original;

ですので上記はコンパイル可能なコードになります。まあこれって要するに original の中身じゃなくて original そのもののピン止めですので、意味はないんですが(String^* とか貰ってもねぇ?)。これの IL を見てみましょう。

pin_ptr<String> が使えれば夢が広がるんですが、コンパイラに却下されます。

  .locals (string& pinned /* modopt は省略 */ V_0)
IL_0000:  ldarga.s   original
IL_0002:  stloc.0
IL_0003:  ret

先ほどの C# の IL と異なるのは、ldarga.s です。original そのものではなくそのアドレスをロードしてるわけですね。

さて、それでは C# が出力するコードに近くなるよう C++/CLI で書いてみましょう。

static void Sample2(String^ original) {
    // まず、String^ のポインタを取得
    pin_ptr<String^> str_ptr = &original;
    // 参照のポインタなんだからそれってポインタのポインタと考えていいよね
    Byte** byte_ptr_ptr = reinterpret_cast<Byte**>(str_ptr);
    // 今度はそのポインタ自体じゃなくてその指す先をピン止め
    pin_ptr<Byte> byte_ptr = *byte_ptr_ptr;
    // ポインタに秘密の数字を加算
    byte_ptr += System::Runtime::CompilerServices::RuntimeHelpers::OffsetToStringData;
    // 目的の型にキャストして終了
    wchar_t* pointer = reinterpret_cast<wchar_t*>(byte_ptr);
    // あとはこの pointer を使ってお好きに
}

さて、割と複雑になりました。ですので文字列を与えてポインタを受け取るような関数にしたいところですが、残念ながらそういう関数にはできません。考えてみれば当然の話ですが、pin_ptr でピン止めできるのはローカル変数のみで、関数を超えてピン止めすることはできないので。そうするには GCHandle が必要です。

Managed C++ の時代から、Microsoft が提供する SDK のヘッダファイルの中には vcclr.h というのがあり、ここで PtrToStringChars というインライン関数が定義されています。やってることを簡単に説明すると、String*(String^) を直接 Byte*(interior_ptr<Byte>)にキャストして OffsetToStringData を加えてるだけで、ピン止めのことが考慮に入っていません。インターンプールの文字列なら移動されることなんてないような気もしますが、そうでない文字列もあるわけで、いったいこれは妥当なのかどうなのか。とりあえず私は怖いから使いません。

となると C/C++ として基本の発想はマクロでしょう。と言うことで、以下のようなマクロを定義してみました。

// 変数名が重複しないようにするための仕込み
#define CONCAT(x, y) CONCAT2(x, y)
#define CONCAT2(x, y) x##y
// wchar_t* に変換するための引数省略版マクロ
#define TO_STRPTR_WCHAR(STRING_PTR, STRING) \
	 TO_STRPTR_TYPED(wchar_t, STRING_PTR, STRING)
// Byte* に変換するための引数省略版マクロ
#define TO_STRPTR_BYTE(STRING_PTR, STRING) \
	 TO_STRPTR_TYPED(Byte, STRING_PTR, STRING)
// 任意の型のポインタに変換するためのマクロ
#define TO_STRPTR_TYPED(TYPE, TYPED_PTR, STRING) \
	 TO_STRPTR_TYPED_LOCAL(TYPE, TYPED_PTR, STRING, \
                          CONCAT(STRING_REF_PTR_, __LINE__), \
                          CONCAT(BYTE_PTR_PTR_, __LINE__), \
                          CONCAT(BYTE_ARRAY_PTR_, __LINE__))
// ローカル変数名をどうにかしたマクロ。内部使用向け
#define TO_STRPTR_TYPED_LOCAL(TYPE, TYPED_PTR, STRING, \
                              STRING_REF_PTR, BYTE_PTR_PTR, \
                              BYTE_ARRAY_PTR) \
	 pin_ptr<String^> STRING_REF_PTR = &((STRING)); \
	 Byte** BYTE_PTR_PTR = reinterpret_cast<Byte**>(STRING_REF_PTR); \
	 pin_ptr<Byte> BYTE_ARRAY_PTR = *BYTE_PTR_PTR; \
	 BYTE_ARRAY_PTR += System::Runtime::CompilerServices:: \
                         RuntimeHelpers::OffsetToStringData; \
	 TYPE* TYPED_PTR = reinterpret_cast<TYPE*>(BYTE_ARRAY_PTR);

// 使用例
String^ original = "sample";
// _WCHARではマクロの第一引数の名前の変数が暗黙に宣言される
// wc は wchar_t* 型
// ただし、wc は NULL 終端されてない可能性がある点に注意
TO_STRPTR_WCHAR(wc, original);
// _TYPED ではマクロの第一引数の型を持つ第二引数の名前の変数
// 型は const 指定もできる
TO_STRPTR_TYPED(const void, pointer, original);
memcpy(target, pointer, original->Length * 2);

pin_ptr の有効範囲がスコープ内限定のためスコープを分けて変数宣言するわけにも行かないので、__LINE__ マクロを使って変数名を行番号縛りにしています。これで一つのスコープ内に複数の TO_STRPTR_TYPED マクロを並べることができます。そのために CONCAT マクロと CONCAT2 マクロを用意し、変数名を結合させています。このマクロ名はほかで使われてそうなので名前を変えたほうがいいかも。

とりあえず書いてみましたが、ここはこの方が良い、とかそもそもこんなの不要でこれを使えば良い、とかあればぜひお知らせください。/CLI を含め、C++ にはあまり詳しくないものですので。



posted by Hongliang at 14:43| Comment(6) | TrackBack(0) | C++/CLI | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
http://support.microsoft.com/kb/311259/ja
ここでは、PtrToStringCharsの戻り値をpinしています。MSが載せていることですし、これで十分ではないでしょうか。
Posted by egtra at 2008年01月14日 12:36
その後結局ラッパ作成がポシャったので C++/CLI を放置してたのですが、改めて調べてみると確かに問題無いものになっているようです。
ご指摘ありがとうございました。
Posted by Hongliang at 2008年02月05日 22:08
O型へのpin止めに自信が無かった(何しろ、一見問題なく動く)ので、同じような事を考えてる一例として助かりました。
さて、本当にpin止めされているのか?どうやって確かめればいいかな...
Posted by kekyo at 2012年01月17日 00:18
彼女には言いづらいプレイなどはセ フ レに頼んでみよ★けどセ フ レといえども容姿は大事!ここに集まる娘達はみんな自分の容姿に自信のある娘達ばかりです!その証拠にプロフ動画まであげている娘もいるほど。なので失敗少なく相手探しができますよ
Posted by 巨 乳 セ フ レタダのり! at 2012年02月02日 10:27
キタキタキタ!!!!遂にこのサイトが始動しました!乳好きの為の乳好きによるマニアも涙もんの乳専用サイトだ!!!!ぷるるん!今回は日本国内にとどまらず世界中の乳動画像を収集!やっぱ世界はすげぇ!W
Posted by 世界プルルン滞在記 at 2012年02月14日 18:55
完全素人娘限定!写メ付き直メ付き!しかも無料!上手にやり取りすれば相手に困る事のない潤った生活が約束されます!清楚系からGAL系まであなたが今一番遊びたい子と仲良くなって下さいね★
Posted by Love Search at 2012年02月16日 05:40
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。