P/Invoke で Windows API を呼び出す際、引数に構造体を渡すには、割と色々な方法があります。
最近ちょっと混乱したので、改めてまとめてみました。
これまで知らなかった便利な方法も紹介しています。
新機能でも何でもありませんが、よく P/Invoke を使われる方にはご一読頂いて損はない内容だと思います。
ちなみに、本記事では、C# で言う struct と class を総称して「構造体」と呼ぶこととします。
struct と class の区別が必要な場合は「struct」と「class」または「値型」と「参照型」と呼び分けます。
本文中では「struct ≠ 構造体」ということにご留意ください。
なお、本記事の内容は、Windows API の呼び出しに限定した内容になっています。
COM の場合はまた事情が違うのですが、そちらは割愛させて頂きます。
struct と class
C# では、struct と class は全く別物です。値型と参照型という違いがあります。
一方、C++ では、struct と class はほとんど同じものです。
違いは、メンバーの可視性を省略した時、struct は既定で public、class は既定で private になるというだけです。
ここから、C++ において struct は単なるデータの集合体、class はカプセル化を強く意識したものという意味を見出すことはできますが、コンパイルしてしまえば区別はなくなります。
ですから、C++ 側で struct だからといって、C# 側でも struct で宣言しなければいけないということは全くありません。class でもいいのです。
書式指定
string.Format のことではありません。
P/Invoke において「書式指定」とは、型に StructLayoutAttribute をつけ、さらに、LayoutKind.Sequential または Explicit を指定することです。
書式指定されていない構造体は P/Invoke で受け渡しすることができません。
StructLayoutAttribute は、その名前に反して class にもつけることができます。
C# では struct には暗黙的に Sequential が指定されています。class では明示的に指定する必要があります。
通常は Sequential で問題ありません。
C++ 側で共用体(union)として定義されているものに対応する場合のみ、Explicit を使う必要があります。
目的から考える
Windows API に構造体を渡したい目的というのは、3 つしかありません。
- 関数に入力値を渡す
- 関数から出力値を受け取る
- その両方
いずれにせよ、通常、構造体はポインターで渡されます。
それ以外の渡し方は後述します。
4 つの渡し方
C#(.NET)では、値型と参照型があります。さらに値渡しと参照渡しがあるので、その組み合わせで以下の 4 通りがあります。
- 値型の値渡し
- 参照型の値渡し
- 値型の参照渡し
- 参照型の参照渡し
P/Invoke の構造体渡しで使うのは基本的に「参照型の値渡し」です。
つまり struct ではなく class を使います。
C++ の言い方で言えば「ポインターの値渡し」になります。
入力値を渡す
C# 側はこう。
[StructLayout(LayoutKind.Sequential)] public class Hoge { // ... } [DllImport("hoge.dll")] public static extern void HogeFunction(Hoge hoge);
呼び出すときはこう。
var hoge = new Hoge { // ... }; HogeFunction(hoge);
struct Hoge { // ... } void WINAPI HogeFunction(const Hoge * hoge);
これはまぁ、特に悩むこともないかと思います。
出力値を受ける
C# 側はこう。
[StructLayout(LayoutKind.Sequential)] public class Hoge { // ... } [DllImport("hoge.dll")] public static extern void HogeFunction([Out] Hoge hoge);
呼び出すときはこう。
var hoge = new Hoge();
HogeFunction(hoge);
C++ 側は const がないだけです。
struct Hoge { // ... } void WINAPI HogeFunction(Hoge * hoge);
入出力
続けて見てしまいましょう。
C# 側はこう。
[StructLayout(LayoutKind.Sequential)] public class Hoge { // ... } [DllImport("hoge.dll")] public static extern void HogeFunction([In][Out] Hoge hoge);
呼び出すときはこう。
var hoge = new Hoge { // ... }; HogeFunction(hoge);
C++ 側は同じです。
struct Hoge { // ... } void WINAPI HogeFunction(Hoge * hoge);
[In] と [Out]
C#er には見慣れない属性かも知れません。
それぞれ、InAttribute と OutAttribute です。
合わせて「方向属性」と呼ばれるこれらは、データのマーシャリングを行う方向を示す属性です。
マーシャリング
何気なく使っている言葉ですが、意味を知っているでしょうか。
何となく、「P/Invoke する時の関数宣言やデータ型の読み替えの規則」のように思われている方もいるのではないかと思いますが、違います。
Marshaling とは「整列」という意味の英単語で、元は COM で使われていた用語です。
ここでは COM の話は省きましょう。
C# から C++ の関数を呼び出してデータをやり取りする時、それぞれの環境において、メモリ上のデータの表現というのは必ずしも同じではありません。
たとえば構造体内に文字列を含むとすると、C++ 側は単純な文字配列へのポインターに過ぎませんが、C# 側は Length などのプロパティを持ちます。
そのため、.NET の string オブジェクトを指すポインターは、C++ での文字列ポインターとしては使えないのです。
そのままでは渡すことができないないので、相手側に合わせてデータの形式を変換する必要があります。
この際、呼び出し側のデータのコピーを作って、それを相手側にとって都合のいい形式に変換してから渡します。
これを「マーシャリング」と呼びます。
要するに「シリアライズ」と似たような意味です。「整列」と「直列化」、似ていますよね。
しかし、マーシャリングはそれなりにコストがかかる処理です。*2
そのため、必要な時に必要な方向(入力方向/出力方向)でのみ行われるように指定することができます。
入力方向のマーシャリングを行うよう指定するのが [In]、出力方向のマーシャリングを行うよう指示するのが [Out] です。
どちらもつけない場合、既定で入力方向のマーシャリングが行われます。そのため、[In] だけをつける意味はありません。
[Out] だけをつけると出力方向のマーシャリングしか行われなくなるので、入出力両方でマーシャリングをしたい場合に [In] と [Out] を両方つけるのです。
コピーが作られるということ
基本的に C# と C++ の間でやり取りされるデータは、マーシャリングするために、一旦コピーが作られます。
C++ から C++ の関数を呼び出すときに、(入力のみであっても)構造体をポインターで渡すのは、値渡しにすると不要なコピーが作られて効率が悪くなるからです。
しかし、C# から C++ のコードを呼び出すときには、コピーを作らなければなりません。基本的にこれは必要なコピーなのです。
C++ 側に渡されるのは、そのコピーのポインターです。
関数から値を出力する時には、C++ 側の関数はコピーの値を書き換えます。
出力方向のマーシャリングをすると指定されていれば、ランタイムは、それをまた C# 側にコピーするのです。
図で表してみましょう。
これは引数を通じた入出力の場合の例です。つまり方向属性として [In][Out] を付けていた場合です。
- マネージド構造体のコピーがアンマネージドメモリ内に作られる(入力方向のマーシャリング)
- コピーを指すポインターがアンマネージド関数に渡される
- アンマネージド関数はコピー領域を更新する
- 更新されたコピー領域の内容がマネージドメモリ内に書き戻される(出力方向のマーシャリング)
元々のマネージド構造体と、アンマネージド領域内に作られるコピーは異なるため、ポインター渡しであっても、方向属性を正しくつけなければデータは受け渡しされません。
out や ref は?
C# には、引数を「参照渡し」する機能があります。引数につける out や ref がそれです。
引数経由で関数から結果を受け取る時には out を使うことが多いと思います(int.TryParse とか)。
これまで「参照型の値渡し」を見てきましたが、「値型の参照渡し」でも、C++ 側の表現は同じポインターになります。
こういう例ですね。
// struct の場合は StructLayout 属性は要らない(暗黙的に Sequential) public struct Hoge { // ... } [DllImport("hoge.dll")] public static extern void HogeFunction(out Hoge hoge);
つまり、どっちでも目的を達することはできます。
この場合、方向属性をつけなくても、out をつければ [Out] が、ref をつければ [In][Out] が指定されたのと同じようにマーシャリングが行われます。
では、なぜ本記事は「参照型の値渡し」を推しているかというと、「値型の参照渡し」にはないメリットがいくつかあるからです。
なお、先の例では、アンマネージド関数から出力を受ける場合でも、([Out] は使いましたが)out はつけませんでした。
最初は、出力を受けるのに out を使わないのは何となく気持ちが悪い…とも思いましたが、考えてみれば .NET 内であっても、関数からの出力を受け取るために「参照型の値渡し」をしている箇所はある*3ので、気にしないことにしました。
null を渡す
Windows API の関数には、しばしば NULL*4を渡すことがあります。
入力値を使用しない場合、デフォルト値を表す場合、出力を必要としない場合などです。
しかし、値型の引数に null を渡すことはできません。Nullable<T> 型を使ってもダメです(ジェネリック型はマーシャリングできません)。*5
参照型であっても out や ref のついた引数には null を渡せません(そもそも、参照型に out や ref をつけるとポインターのポインターになってしまうため型が不一致になってしまいます)。
参照型の値渡しであれば、普通に null を渡すことができます。
これを値型の参照渡しでやろうと思うと、NULL を渡したい引数だけ IntPtr にしたオーバーロードを作らなければなりません(IntPtr.Zero が NULL ポインターとして利用できます)。
public struct Hoge { // ... } // 有効な値を渡す場合 [DllImport("hoge.dll")] public static extern void HogeFunction(out Hoge hoge); // NULL を渡す場合 [DllImport("hoge.dll")] public static extern void HogeFunction(IntPtr hoge);
この例では引数が 1 つしかありませんが、例えば引数が 3 つある関数だと、第 1 引数だけ IntPtr にする、第 2 引数だけ、第 3 引数だけ、第 1 と第 2 …のように、6 通りものオーバーロードを作るのは苦痛です。*6
【追記:2021-03-05】Unsafe.NullRef<T>
.NET 5.0 から、Unsafe.NullRef<T> という機能が増えました。これを使うと、なんと「値型の null 参照」が作れるようなのです(未検証)。
なお、これは Blittable 構造体でのみ使用可能とのことです。Blittable 構造体については、このすぐ後で解説しています。
固定による最適化
これも、参照型の値渡しでのみ得ることができる恩恵です。
これを説明するためには、まず Blittable 型と非 Blittable 型について説明する必要があります。
Blittable 型
MSDN に説明がありますが、要約します。
まず、以下の型は Blittable 型です。
- byte
- sbyte
- short
- ushort
- int
- uint
- long
- ulong
- IntPtr
- UIntPtr
- float
- double
さらに、Blittable 型の固定長*7一次元配列と、Blittable 型のみを含む、書式指定された構造体も Blittable 型です。
以下のような型は Blittable 型ではありません。
- char
- bool
- decimal
- string
- object
従って、string を含む構造体も Blittable 型ではありません。
で、結局、これらは何なのでしょうか?
先程、マーシャリングについて説明した時に、こう言いました。
C# から C++ の関数を呼び出してデータをやり取りする時、それぞれの環境において、メモリ上のデータの表現というのは必ずしも同じではありません。
このため、相手方の都合に合わせたデータのコピーを作る(マーシャリング)必要があるのでしたね。
このようにマーシャリングする必要のある型が、非 Blittable 型です。
逆に言えば、C# と C++ でメモリ上のデータの表現が同じ型が Blittable 型ということになります。
ちなみに blit というのは「転送する」という意味だそうです。
C++ で Windows アプリを組んだことがあればほぼ確実に使ったことがあるであろう BitBlt 関数の "Blt" はこの略です。*8
blit は "block transfer" の略らしいです*9が、Blittable 型の場合には「転送」する必要がありません。
再び、固定による最適化
Blittable 型はマーシャリングのためのコピーを作る必要がありません。
ということは、アンマネージド関数に、マネージドメモリのポインターを直接渡すことができるということです。
- マネージド構造体を指すポインターがアンマネージド関数に直接渡される
- アンマネージド関数はマネージド構造体を直接書き替える
コピーが発生しないためパフォーマンスが向上します。
この場合は、[In][Out] 属性をつけなくても、データの入出力が可能です。
ただし、この暗黙の動作を期待するべきではなく、必要に応じて、きちんと [In][Out] 属性をつけるべきです。*10
ところで、.NET にはガベージ コレクション(GC)があります。
GC は、不要なメモリ領域を解放するだけでなく、コンパクションという作業を行います。
コンパクションとは、複数の小さな空き領域を統合して、大きな一つの空き領域にすることです。
こうすることで、より大きなオブジェクトを確保することが可能となります。
- メモリ上に小さなオブジェクトがたくさん存在する状態
- いくつかのオブジェクトが GC によって解放され、小さな空き領域(白)が複数ある状態
- この状態で右側にある大きなオブジェクトを確保しようとしても、大きな空き領域がないため確保できない
- メモリのコンパクションが行われた状態
- 空き領域が統合されて大きな空きができたため、大きなオブジェクトが確保できる
コンパクションが行われると、いくつかの色のついたメモリ領域が移動していることがわかります。
移動しているということはアドレスが変わっているということです。
通常、コンパクションが行われても、.NET ランタイムはオブジェクトの移動先の位置を把握しているので問題はありません。
ただし、P/Invoke の場合はそうは行きません。
アンマネージド関数は、コンパクションが行われたことなど知らず、移動先の位置も把握していないからです。
そのまま結果を出力すれば、予期せぬ位置のメモリを破壊してしまいかねません。
そこで、Blittable 型の場合には「固定」という処理が自動的に行われます。
これは、特定のメモリブロックを、GC の際に発生するコンパクションから除外し、元の位置に留め置くことです。
紫色のメモリブロックはアンマネージド関数で使われている(という想定)ので移動していません。
ただし、図からもわかるように、固定はコンパクションを阻害します。
とは言え、一般にアンマネージド関数を呼び出すために固定している時間は短時間なので、問題になることはないでしょう。
まとめ
以上のように、「参照型の値渡し」には「値型の参照渡し」にはないメリットがあります。
- null を渡すことができる
- 固定による最適化の恩恵を受けられる
そのため、
- 構造体を渡す際は「参照型の値渡し」を使用する
- [StructLayout] [In] [Out] といった属性をきちんとつける
- out や ref は使用しない
といったことを意識するとよいでしょう。
おまけ
ここからはオマケです。
典型的なケースでは原則として、「参照型の値渡し」を使えばよいのですが、そうでないケースというのもいくつかあります。
値型の値渡し
構造体ではなく整数などのプリミティブ型は「値型の値渡し」で渡されます。
が、構造体も「値型の値渡し」で渡す関数も、少数ながらあります。
例えば、BitBlt 関数の半透明版である AlphaBlend 関数などがそれです。
この関数の第 4 引数である BLENDFUNCTION 構造体は値渡しで渡されます。
この場合、BLENDFUNCTION 構造体は Blittable 型ですが、値渡しなのでコピーが作られます。アンマネージド関数側で書き替えることはできません。
ポインターのポインター
Windows API の関数から出力を受け取るには、これまで見てきたように、呼び出し側で構造体を用意してそのアドレスを渡すパターンの他に、もう一つのパターンがあります。
それは、アンマネージド関数の側で必要なサイズのメモリ領域を確保し、そのポインターを返すというケースです。
この場合、呼び出し側では、ポインターのポインターを渡す必要があります。
たとえば Windows API のエラーコードからエラーメッセージを取得する FormatMessage 関数などが該当します。*11
この場合、確保されたメモリ領域は、呼び出し側で LocalFree 関数を使って解放する必要があります。
.NET にとっては面倒くさいことに、LocalFree 関数には、Marshal クラスに相当するメソッドがありません。
そのため、LocalFree 関数も P/Invoke で呼び出さなければなりません。
C# 側では、「参照型の参照渡し」が「ポインターのポインター」に相当します。
では、FormatMessage を使う場合は out string とすれば良いのかというと、そうではありません。
それでは、結果は受け取れるのですが、そのメモリ領域を解放することができないからです。string オブジェクトとして得られるのはマーシャリングされた後の値であり、そこから、アンマネージド領域内に確保された元のメモリ領域のアドレスを得る手段はありません。
そのため、この場合は以下のようにしなければなりません。
[DllImport("kernel32.dll", EntryPoint = "FormatMessageW")] public static extern int FormatMessage( int flags, IntPtr source, int messageId, int languageId, out IntPtr buffer, int size, IntPtr arguments);
呼び出す場合は、こう。
IntPtr buffer = IntPtr.Zero; try { int result = NativeMethods.FormatMessage( /* フラグは面倒なので省略 ... */, IntPtr.Zero, Marshal.GetLastWin32Error(), 0, out buffer, 0, IntPtr.Zero); if (result != 0) { string message = Marshal.PtrToStringUni(buffer); } } finally { if (buffer != IntPtr.Zero) { NativeMethods.LocalFree(buffer); } }
まぁ、実際には .NET で FormatMessage を使う機会はそんなにないと思いますが…。
【追記:2021-03-05】Marshal.FeeHGlobal
Marshal.FreeHGlobal メソッドの実態は LocalFree 関数だと書いてありました。
UnmanagedType.LPStruct
パラメーターや戻り値、構造体のメンバーなどに MarshalAsAttribute をつけることで、マーシャリングを詳細に制御することができます。
UnmanagedType 列挙体で、マーシャリング用のコピーのデータ形式を指定します。
よく使うのはこの辺りでしょうか。
UnmanagedType | Windows API の型 |
---|---|
Bool | BOOL*12 |
LPWStr | LPWSTR、LPCWSTR |
U4 | DWORD、ULONG |
さて、そんな UnmanagedType の中に、LPStruct というのがあります。構造体のポインター、のようですね。
これを付けると、C# 側では値渡ししているものが、C++ 側ではポインター渡しとして扱われます。
C# 側から渡した値がコピーされて、そのポインターが C++ 側に渡されるわけです。
C# 側では値渡しなので、C++ 側で書き替えることはできません。
これは一体何に使うのかと言うと、典型的には、アンマネージド関数に GUID を渡す場合です。特に COM API で良く使われます。
GUID は構造体で、アンマネージ側では REFCLSID や REFIID 等といった型で扱われます。REF は Reference の略で、これらはいずれも GUID * と同じです。
一方、.NET の Guid は値型なので、「値型の参照渡し」で渡さなければいけません。ref Guid ということです。
ところで、GUID というのは、大抵、定数的に扱われるものです。
そのため、心情としては、static readonly フィールドとして宣言したいところです。
しかし、C# の仕様上、readonly なフィールドを ref で渡すことはできないのです。
そこで、アンマネージド関数側で書き替えることなどないのでまぁいいか…と割り切って、readonly を外すということが多いのではないでしょうか。
こういう場合に、Guid 型の引数に LPStruct をつけておくと、C# 側では値渡しなので readonly フィールドの値を渡すことができ、C++ 側ではポインターとして受け取れるということなのです。
こういう特殊なケース以外では活用する局面はないと思います。
*1:余談ですが個人的には Hoge const * とするほうが好きです。
*2:特に COM でリモートプロセスやリモートマシンに対して実行する場合には。
*3:Stream.Read とか
*5:Nullable は特別扱いしてほしいですよね…
*6:必ず 6 通りのオーバーロードを作らなければいけないわけではありません。C# 側から実際に使用するパターンのみ作れば十分です。
*7:呼び出し側であらかじめ長さを把握して確保してあること
*8:なのでこの関数名は「ビットビルト」ではなく「ビットブリット」なのです。
*9:だとすると「i」がどこから出てきたのかわかりませんね。blt を英単語っぽくするために母音を入れてしまったのでしょうか。
*10:COM の場合に問題が発生します。
*11:FORMAT_MESSAGE_ALLOCATE_BUFFER フラグを指定した場合
*12:BOOLEAN ではない。BOOL は 4 バイト、BOOLEAN は 1 バイト。