P/Invoke時におけるマーシャラの動作(マーシャリング)

P/Invoke において,データをC#側からアンマネージ側へ渡す,またはアンマネージ側から受け取るとき,どのような動作をしているのかについて調べてみました.

まず P/Invoke について,MSDN の中で参考になりそうなページを挙げます.
クラス、構造体、および共用体のマーシャリング
文字列のマーシャリング
型の配列のマーシャリング
各種のマーシャリング
プラットフォーム呼び出しのデータ型


これらのドキュメントを読んでいると,値型,参照型,blittable 型,非 blittable 型といった,型に関する用語が出てきます.
以下にまとめてみました (blittable の語源だけはわかりませんでしたが...).

値型 (値型)

値型とは,System.ValueType から派生する型のことであり,インスタンスがスタック上に割り当てられます.
また値型の変数は,参照型の変数のようにオブジェクトへの参照を持つのではなく,変数がデータを直接格納します(newする必要がないということ).
値型には,

があります.

その他参考:http://msdn2.microsoft.com/ja-jp/library/34yytbws(VS.80).aspx

参照型

参照型は,値型以外の型のことであり,インスタンスがヒープ上に割り当てられます.
また参照型の変数は,ヒープ上のオブジェクトへの参照を格納します.
参照型には,

があります.
さらに自己記述型は,配列型とクラス型の2つに分けられます.
クラス型とは,ユーザー定義のクラス,ボックス化された値型,およびデリゲートのことを指します.

その他参考

http://msdn2.microsoft.com/ja-jp/library/3ewxz6et(VS.80).aspx
http://msdn2.microsoft.com/ja-jp/library/zcx1eb1e(VS.80).aspx
http://msdn2.microsoft.com/ja-jp/library/2hf02550(VS.80).aspx


また,上記のような分類とは別に,マーシャリングの観点から以下のような分類ができます.

Blittable 型

blittable 型とは,マネージメモリ上でも,アンマネージメモリ上でも表現が共通している型のことをいいます.
blittable 型の一覧は,下記ページに記載されています.
http://msdn2.microsoft.com/ja-jp/library/75dwhxf7(VS.80).aspx

また,これらの一次元配列や,blittable 型のみを含む書式指定された (StructLayout 属性で Sequential またはExplicit と指定された) 値型およびクラス型も blittable 型です (多次元配列は異なるようです).

非 Blittable 型

非 blittable 型とは,マネージメモリ上とアンマネージメモリ上とでは表現が異なる,または曖昧 (条件によって同じであったり異なったりする) である型のことをいいます.
非 blittable 型をマネージコードとアンマネージコードの間でマーシャリングするときは,データ表現を変換する必要が生じることがあります.
http://msdn2.microsoft.com/ja-jp/library/75dwhxf7(VS.80).aspx

In/Out 属性

P/Invoke 宣言でパラメータの方向属性を指定するものに,InAttribute,OutAttribute があります.
http://msdn2.microsoft.com/ja-jp/library/77e6taeh(VS.80).aspx

以下,「呼び出し先」はアンマネージDLLの関数,「呼び出しもと」はC#側のことを指しています.

  • InAttribute

http://msdn2.microsoft.com/ja-jp/library/system.runtime.interopservices.inattribute(VS.80).aspx
MSDNからの引用です.

呼び出し元から呼び出し先へデータをマーシャリングするが、逆方向にはマーシャリングしないことを示します。
...
InAttribute と OutAttribute を組み合わせると、配列型および書式付きの非 blittable 型に適用する場合に、特に役立ちます。
両方の属性を適用した場合にだけ、呼び出し元は、呼び出し先がこれらの型に対して加えた変更を参照できます。
これらの型はマーシャリング中にコピーを要求するため、InAttribute と OutAttribute を使用して、不要なコピーを減らすことができます。

  • OutAttribute

http://msdn2.microsoft.com/ja-jp/library/system.runtime.interopservices.outattribute(VS.80).aspx
MSDNからの引用です.

呼び出し先から呼び出し元へ、データをマーシャリングすることを示します。
...
参照渡しの値型または参照型に OutAttribute を適用することによって、In/Out の動作を Out のみの動作に変更できます。
これは、C# で out キーワードを使用することに相当します。
たとえば、値渡しの配列 (既定で In のみのパラメータとしてマーシャリングされる) は、Out のみに変更できます。
ただし、相互運用マーシャラは固定を使用するため、要素またはフィールドがすべて blittable の型の場合、この動作は期待されるセマンティクスを提供しないことがあります。
データを呼び出し先に渡すかどうかが問題にならない場合、非 blittable 型については Out のみのマーシャリングの方がパフォーマンスに優れています。


マーシャラは,パラメータの型やその修飾子に基づいて方向を決定します.
マーシャラの既定の動作を変更する場合に,方向属性を指定するようですね.

ref/out 修飾子

通常はオブジェクトが参照渡しされますが,ref 修飾子を使用すると引数が参照渡しされます (引数として渡された変数そのものがメソッドの中に入っていくイメージ).
そのため,メソッド内部で引数に加えられた変更は,制御が呼び出し元のメソッドに戻された時点で変数に反映されます (メソッド内部で,変数の参照するオブジェクトを別のオブジェクトにする等).
ただし,ref パラメータとして渡される引数は,事前に初期化されている必要があります.

  • 補足

ref 修飾子と類似するものに,out 修飾子があります.
out 修飾子は,メソッド内部で引数にオブジェクトへの参照が格納されることを保証します.
引数として渡す変数は,事前に初期化する必要がありませんが,呼び出し先のメソッド内部では,制御が呼び出し元へ戻る前に値を代入しなければなりません.

コピーと固定 (http://msdn2.microsoft.com/ja-jp/library/23acw07k(VS.80).aspx)

いよいよ本題です.

値型の値渡し (値型をそのまま渡した場合)


アンマネージ側:

extern "C" {
__declspec(dllexport) int __stdcall Test_Int(int n)
{
    return -1 * n;
}

struct __declspec(dllexport) BlittableStruct
{
    int a;
    int b;
};

__declspec(dllexport) void __stdcall Test_BlittableStruct(BlittableStruct bs)
{
    bs.a = -1;
    bs.b = -1;
}
}

マネージ側:

[DllImport("Unmanaged.dll", EntryPoint = "Test_Int")]
public static extern int Test_Int(int n);

[StructLayout(LayoutKind.Sequential)]
public struct BlittableStruct
{
    public int a;
    public int b;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_BlittableStruct")]
public static extern void Test_BlittableStruct1(BlittableStruct bs);

動作説明
データが直接スタックに渡される (コピー).
何も指定しないと,マーシャラの既定動作は In である.
呼び出し先の引数の形式は XXX x でなければならない.

値型の参照渡し (ref 修飾子を付けた場合)


アンマネージ側:

extern "C" {
__declspec(dllexport) void __stdcall Test_Int_Ptr(int* p)
{
    *p = -1;
}

struct __declspec(dllexport) BlittableStruct
{
    int a;
    int b;
};

__declspec(dllexport) void __stdcall Test_BlittableStruct_Ptr(BlittableStruct* p)
{
    p->a = -1;
    p->b = -1;
}
}

マネージ側:

[DllImport("Unmanaged.dll", EntryPoint = "Test_Int_Ptr")]
public static extern void Test_Int_Ptr(ref int n);

[StructLayout(LayoutKind.Sequential)]
public struct BlittableStruct
{
    public int a;
    public int b;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_BlittableStruct_Ptr")]
public static extern void Test_BlittableStruct_Ptr1(ref BlittableStruct bs);

...

int n = 0;
Test_Int_Ptr(ref n);

BlittableStruct bs;
bs.a = 9;
bs.b = 9;
Test_BlittableStruct_Ptr1(ref bs);

動作説明
アンマネージヒープ上にオブジェクトがコピーされ,そのオブジェクトへのポインタがスタックに渡される.
何も指定しないと,マーシャラの既定動作は In/Out である.
呼び出し先の引数の形式は XXX* p でなければならない.
補足
呼び出し先の引数の形式が XXX** pp であっても参照渡しは成功するが,アンマネージ側に渡される値は,アンマネージ側の意図する値ではない (pp がオブジェクトを指すポインタへのポインタではなく,オブジェクトへのポインタになってしまう).
【例】
アンマネージ側:

extern "C" {
struct __declspec(dllexport) BlittableStruct
{
    int a;
    int b;
};
__declspec(dllexport) void __stdcall Test_BlittableStruct_PtrPtr(BlittableStruct** pp)
{
    (*pp) = new BlittableStruct();
    (*pp)->a = -1;
    (*pp)->b = -1;
}
}

マネージ側:

[StructLayout(LayoutKind.Sequential)]
public struct BlittableStruct
{
    public int a;
    public int b;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_BlittableStruct_PtrPtr")]
public static extern void Test_BlittableStruct_PtrPtr1(ref BlittableStruct bs);
...
BlittableStruct bs;
bs.a = 9;
bs.b = 9;
Test_BlittableStruct_PtrPtr1(ref bs);

// この呼び出し後,bs.aはアンマネージ側でnewされたオブジェクトのアドレスが格納され,bs.bは9のままである.
// イミディエイトウィンドウで確認すると,
//   *((BlittableStruct*)bs.a)
//   {Invoker.Tester1.BlittableStruct}
//   a: 0xffffffff    // ← -1
//   b: 0xffffffff    // ← -1
// のように,bs.aの指すオブジェクトに,アンマネージ側の変更が反映されている.

参照型の値渡し (参照型をそのまま渡した場合)

blittable 型と非 blittable 型とで,動作が変わります.
例 (blittable 型)
アンマネージ側:

extern "C" {
__declspec(dllexport) void __stdcall Test_Blittable_Array(int* p)    // 配列のサイズも渡すべき
{
    p[0] = -1;    // 当然のことながら,オーバーランには十分注意する必要がある
}

struct __declspec(dllexport) BlittableStruct
{
    int a;
    int b;
};

__declspec(dllexport) void __stdcall Test_BlittableStruct_Ptr(BlittableStruct* p)
{
    p->a = -1;
    p->b = -1;
}
}

マネージ側:

[DllImport("Unmanaged.dll", EntryPoint="Test_Blittable_Array")]
public static extern int Test_Blittable_Array(int arr);

[StructLayout(LayoutKind.Sequential)]
public class BlittableStruct2
{
    public int a;
    public int b;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_BlittableStruct_Ptr")]
public static extern void Test_BlittableStruct_Ptr2(BlittableStruct2 bs);
...
int arr = new int[10];
...
Test_Blittable_Array(arr);
// ↑この呼び出しで,arrが直接変更されてしまう.

BlittableStruct2 bs2 = new BlittableStruct2();
Test_BlittableStruct_Ptr2(bs2);
// ↑この呼び出しで,bs2が直接変更されてしまう.

例 (非 blittable 型)
アンマネージ側:

extern "C" {
struct __declspec(dllexport) NonBlittableStruct
{
    char* str;
};

__declspec(dllexport) void __stdcall Test_NonBlittableStruct_Ptr(NonBlittableStruct* p)
{
    p->str = "hoge hoge!";
}
}

マネージ側:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public class NonBlittableStruct2
{
    public string str;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_NonBlittableStruct_Ptr")]
public static extern void Test_NonBlittableStruct_Ptr2(NonBlittableStruct2 bs);

...

NonBlittableStruct2 nbs2 = new NonBlittableStruct2();
nbs2.str = "Let's Go!";
Test_NonBlittableStruct_Ptr2(nbs2);
// ↑blittableの時とは異なり,nbs2が直接変更されることはない.
// [Out]を指定すると,マーシャリングにより変更が反映される

動作説明

  • blittable 型:固定され,ポインタがスタックへ渡される
  • 非 blittable 型:アンマネージ側へコピーされ,アンマネージヒープ上のオブジェクトへのポインタがスタックへ渡される

何も指定しないとマーシャラの既定動作はInである.
しかし,blittable 型の場合,変更が呼び出し元のオブジェクトへ反映されてしまう危険性がある (In/Out のようにみえるが,属性的には In のみであるため,おかしい).
もしアンマネージ側のコードが変更を加えるものであれば,明示的に Out 属性を指定しなければならない.
なお非 blittable 型の場合,明示的に Out 属性を指定しないと反映されない.
また,呼び出し先の形式は XXX* p でなければならない.
値型の参照渡しの時と同様,XXX** pでも呼び出しは成功するが,正しくない.

参照型の参照渡し (ref 修飾子を付けた場合)


アンマネージ側:

extern "C" {
struct __declspec(dllexport) BlittableStruct
{
    int a;
    int b;
};

__declspec(dllexport) void __stdcall Test_BlittableStruct_PtrPtr(BlittableStruct** pp)
{
    (*pp) = new BlittableStruct();
    (*pp)->a = -1;
    (*pp)->b = -1;
}
}

マネージ側:

[StructLayout(LayoutKind.Sequential)]
public class BlittableStruct2
{
    public int a;
    public int b;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_BlittableStruct_PtrPtr")]
public static extern void Test_BlittableStruct_PtrPtr2(ref BlittableStruct2 bs);
...
BlittableStruct2 bs2 = new BlittableStruct2();
Test_BlittableStruct_PtrPtr2(ref bs2);

動作説明
呼び出し先へは,「アンマネージヒープ上へコピーされたオブジェクトへのポインタ」を指すポインタが渡される.
呼び出し先から戻るときは,アンマネージヒープ上のオブジェクトの内容を,マネージヒープ上のオブジェクトにコピーすることで,アンマネージ側の変更を反映させる.
何も指定しないとマーシャラの既定動作はIn/Outである.
呼び出し先の形式は XXX** pp でなければならない.

System.String と System.Text.StringBuilder

通常の動作とは異なる部分だけを説明します.
System.String の値渡し

アンマネージ側:

extern "C" {
__declspec(dllexport) void __stdcall Test_AnsiString(char* str)            // const char* str とすべき,サイズを渡すべき
{
    printf("str = %s\n", str);
}

__declspec(dllexport) void __stdcall Test_UnicodeString(wchar_t* str)    // const wchar_t* str とすべき,サイズを渡すべき
{
    str[0] = 'Z';    // このコードは危険!!
}
}

マネージ側:

[DllImport("Unmanaged.dll", CharSet = CharSet.Ansi)]
public static extern void Test_AnsiString([In,Out] string str);

[DllImport("Unmanaged.dll", EntryPoint = "Test_UnicodeString", CharSet = CharSet.Unicode)]
public static extern void Test_Val_UnicodeString(string str);
...
string str = "abc";
Test_AnsiString(str);
Test_Val_UnicodeString(str);    // この呼び出しは危険!

動作説明
CharSet.Unicode として値渡しするときに限り,内部バッファのポインタが呼び出し先へと渡される.
しかし,呼び出し先で内部バッファを変更してしまうと,呼び出し元へ戻ってきた後の String オブジェクトの動作がおかしくなってしまう (String オブジェクトは不変オブジェクト).
呼び出し先で変更を加えたいときは,適切なサイズを確保したStringBuilderオブジェクトを値渡しすべき.
CharSet.Ansi として値渡しするときは,呼び出し側へコピーが渡され,かつ Out 属性を指定してもアンマネージ側で加えられた変更は反映されない (String オブジェクトは不変オブジェクトなので).


System.String の参照渡し

アンマネージ側:

extern "C" {
__declspec(dllexport) void __stdcall Test_UnicodeString_Ptr(wchar_t** pstr)
{
    (*pstr)[0] = 'Z';
}
}

マネージ側:

[DllImport("Unmanaged.dll", EntryPoint = "Test_UnicodeString_Ptr", CharSet = CharSet.Unicode)]
public static extern void Test_Ref_UnicodeString_Ptr(ref string str);
...
string str = "abc";
Test_Ref_UnicodeString_Ptr(ref str);
// str == "Zbc",呼び出し前/後で,strの指すオブジェクトが変化する(ポインタで確認)

動作説明
呼び出し先から制御が戻ってきたとき,アンマネージヒープ上のオブジェクトの内容をマネージヒープ上のオブジェクトにコピーするのではなく,新しい String オブジェクトを生成し,そこにコピーする.
参照渡しした変数は,新しいオブジェクトへの参照を持つことになる (イミディエイトウィンドウで String オブジェクトのポインタを確認すると,呼び出しの前後で変化している).


System.Text.StringBuilder の値渡し

アンマネージ側:

extern "C" {
__declspec(dllexport) void __stdcall Test_UnicodeString(wchar_t* str)
{
    str[0] = 'Z';
}
}

マネージ側:

[DllImport("Unmanaged.dll", EntryPoint = "Test_UnicodeString", CharSet = CharSet.Auto)]
public static extern void Test_Val_UnicodeStringBuilder(StringBuilder sb);
...
StringBuilder sb = new StringBuilder(100);
Test_Val_UnicodeStringBuilder(sb);
// ↑sb[0]に'Z'が入る.

動作説明
StringBuilder オブジェクトの内部バッファへのポインタが,呼び出し側へ渡される.
通常,参照型の値渡しは In ですが,StringBuilder は常に In/Out となる (例外規則).

補足

アンマネージ側から,何かしらのポインタを受け取る場合,C# 側では IntPtr として受け取り,Marshal.PtrToStructure で変換する必要があります.