.NET Frameworkでは属性によって構造体(またはクラス)のフィールドのレイアウトを指定することができます。 例えばFieldOffset属性を使用すれば各フィールドのオフセットを明示的に指定することができます。 また、StructLayout属性を使用すれば構造体のアラインメント(パッキングサイズ)を指定することができます。 アンマネージAPI呼び出しや、構造を持ったバイナリデータを扱う場合にはStructLayout属性とFieldOffset属性が非常に役立ちます。
概略
StructLayout属性とFieldOffset属性を組み合わせることで、構造体のアラインメント、サイズやフィールドのレイアウトを厳密に定義することができます。 まずは、これらの属性で具体的にどのようなことが出来るか、例を挙げて見ていきます。
C#やVBでは言語要素として共用体(union
)を作成する機能は用意されていません。 しかし、構造体内のフィールドのレイアウトを指定するStructLayout属性と、各フィールドのオフセットを指定するFieldOffset属性を使うことで共用体と同等の機能を持つ構造体を作成することができます。
同様に、#pragma pack(n)
のようなアラインメント(パッキングサイズ)を指定する命令も言語やコンパイラの機能としては用意されていませんが、StructLayout属性のPackフィールドによって指定することができます。
また、StructLayout属性のSizeフィールドによってパッキングサイズとは個別に構造体のサイズを指定することができます。
以降で個々の属性や指定できる値について詳しく解説します。
フィールドのレイアウト (StructLayout属性)
StructLayout属性は、メモリ上でのフィールド(メンバ変数)の配置方法を指定するための属性です。 StructLayout属性は構造体だけでなくクラスにも適用することができます。 フィールドの配置方法はLayoutKind列挙体で指定することができ、次のいずれかを指定することが出来ます。
- LayoutKind.Auto
- ランタイムが自動的に最適な順序でフィールドを配置する (StructLayout属性を指定しない場合と同じ)
- LayoutKind.Sequential
- ランタイムによる自動的な並べ替えを行わず、コード上で記述されている順序のままフィールドを配置する
- LayoutKind.Explicit
- 明示的に位置(オフセット)を指定してフィールドを配置する (各フィールドのオフセットを後述のFieldOffset属性で指定する)
構造体にStructLayout属性を適用する場合の例は次のようになります。
LayoutKind.Explicit
を指定した場合、すべてのフィールドに対してFieldOffset属性を指定する必要があります。 また、アンマネージAPI呼び出しの引数として渡される構造体(またはクラス)には、LayoutKind.Sequential
またはLayoutKind.Explicit
のどちらかが指定されている必要があります。
フィールドのオフセット (FieldOffset属性)
FieldOffset属性は構造体(またはクラス)内における各フィールドの位置(オフセット)を指定するための属性です。 構造体にLayoutKind.Explicit
を指定した場合にはすべてのフィールドに対してFieldOffset属性を指定し、オフセットを明示的に指定する必要があります。 この属性では、構造体の先頭からのオフセット値をバイト単位で指定します。
FieldOffset属性を指定している場合でも、フィールドの値の参照や設定は通常のフィールドと同じように行うことができます。
この結果にもあるとおり通常の構造体の場合と何ら変わりありません。 しかし、メモリ上の配置は次の図のようになっているはずです。 (図はリトルエンディアン環境でのものです)
オフセット (バイト) |
フィールド | 値 |
---|---|---|
0 | F1 | 0x33 |
1 | 0x22 | |
2 | 0x11 | |
3 | 0x00 | |
4 | F2 | 0x77 |
5 | 0x66 | |
6 | 0x55 | |
7 | 0x44 |
共用体の実装
FieldOffset属性では、他のフィールドと同じオフセットを指定することもできます。 つまり、複数のフィールドが同一のメモリ領域を参照するようにオフセットを指定することができます。 これにより、FieldOffset属性を使って共用体(union
)と同じ構造を作ることができます。 C#やVBでは共用体を直接作成する言語機能はありませんが、構造体とFieldOffset属性を組み合わせることによって共用体となる構造体をつくることができます。
例として上位ワード(2バイト)と下位ワードを参照するフィールドと、ダブルワード(4バイト)を参照するフィールドを持つ共用体を作成すると次のようになります。
実行結果からも共用体と同等の動作となっていることが分かります。 メモリ上の配置は次のようになっているはずです。
オフセット (バイト) |
フィールド | 値 | |
---|---|---|---|
0 | Value | LoWord | 0x44 |
1 | 0x33 | ||
2 | HiWord | 0x22 | |
3 | 0x11 |
参考までに、これと同等の結果を得るためのC++コードを次に示します。
フィールドのオフセットの取得 (Marshal.OffsetOf)
C/C++のoffsetof()
のように、フィールドのオフセットを取得したい場合はMarshal.OffsetOfメソッドを使用します。 このメソッドはFieldOffset属性で明示的にオフセットを指定していないフィールドでもオフセットを取得することができるため、実行時までオフセットがわからないフィールドのオフセットも取得することができます。
Marshal.OffsetOfメソッドではオフセットを取得したい型をType型、フィールド名を文字列で指定します。 オフセットはIntPtr型としてポインタの形で返されるため、数値として取得したい場合はさらにIntPtr.ToInt32メソッドを呼び出すなどして変換する必要があります。
この結果からもわかるように、アラインメントによってフィールドのオフセットが異なる場合もあります。 アラインメントの指定については後述の§.アラインメントの指定 (StructLayoutAttribute.Pack)を参照してください。
.NET Framework 2.0以降では、非パブリックフィールドのオフセットも取得することができます。
.NET Framework 4.5.1以降では、オフセットを取得したい型(Type
)を引数ではなく型パラメータとして指定することもできます。
Marshal.OffsetOfメソッドではフィールド名を文字列で指定するため、取得したいフィールドの名前は既知である必要があります。 名前が未知の任意のフィールドについてオフセットを取得したい場合は、次の例のようにリフレクションによってフィールド名(FieldInfo.Name)を取得してからMarshal.OffsetOfメソッドを呼び出すようにします。
Type.GetFieldsメソッドについてはリフレクション §.メンバ情報の取得 (MemberInfo)を参照してください。
アラインメントの指定 (StructLayoutAttribute.Pack)
構造体(またはクラス)のフィールドのアラインメント(パッキングサイズ)を変更するにはStructLayout属性でPackフィールドを指定します。 Pack
を指定した場合、各フィールドはPackで指定された値の倍数のオフセットに配置されます。 例えば2を指定すれば各フィールドのオフセットは2の倍数となります。 Pack
に指定できる値は2nである値のうち、0, 1, 2, 4, 8, 16, 32, 64, 128のいずれかです。 0を指定した場合は、デフォルトと同じ、つまりPack
を指定しなかった場合と同じになります。
この結果からもわかるとおり、各フィールドは次のように配置されているはずです。
オフセット (バイト) |
フィールド |
---|---|
0 |
byte F1
|
1 |
int F2
|
2 | |
3 | |
4 | |
5 |
int F3
|
6 | |
7 | |
8 |
オフセット (バイト) |
フィールド |
---|---|
0 |
byte F1
|
1 | 未使用 |
2 |
int F2
|
3 | |
4 | |
5 | |
6 |
int F3
|
7 | |
8 | |
9 |
オフセット (バイト) |
フィールド |
---|---|
0 |
byte F1
|
1 | 未使用 |
2 | |
3 | |
4 |
int F2
|
5 | |
6 | |
7 | |
8 |
int F3
|
9 | |
10 | |
11 |
なお、.NET FrameworkおよびMonoのデフォルトではアラインメントは4
となるようです。
サイズの指定 (StructLayoutAttribute.Size)
StructLayout属性でSizeフィールドを指定することにより、構造体のサイズを明示的に指定することができます。 例えば、サイズは固定されているが、具体的なフィールドは割り当てられていない構造体を作成したい場合などに使用します。 Packフィールドとは異なり、Size
フィールドには2nだけでなく任意の値を指定することができます。
構造体のサイズの取得については構造体のサイズも参照してください。
構造体内で定義されているフィールドの総サイズがSize
フィールドで指定している値を超える場合、構造体のサイズは当然Size
フィールドで指定している値よりも大きくなります。 コンパイルエラーや実行時エラーは発生しないため、意図しない動作の原因となりうることに注意が必要です。