.NET Frameworkでは属性によって構造体(またはクラス)のフィールドのレイアウトを指定することができます。 例えばFieldOffset属性を使用すれば各フィールドのオフセットを明示的に指定することができます。 また、StructLayout属性を使用すれば構造体のアラインメント(パッキングサイズ)を指定することができます。 アンマネージAPI呼び出しや、構造を持ったバイナリデータを扱う場合にはStructLayout属性FieldOffset属性が非常に役立ちます。

§1 概略

StructLayout属性とFieldOffset属性を組み合わせることで、構造体のアラインメント、サイズやフィールドのレイアウトを厳密に定義することができます。 まずは、これらの属性で具体的にどのようなことが出来るか、例を挙げて見ていきます。

C#やVBでは言語要素として共用体(union)を作成する機能は用意されていません。 しかし、構造体内のフィールドのレイアウトを指定するStructLayout属性と、各フィールドのオフセットを指定するFieldOffset属性を使うことで共用体と同等の機能を持つ構造体を作成することができます。

共用体の構成
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
struct DWORD {
  // ダブルワード(オフセット0から4バイト分)を格納・参照するフィールド
  [FieldOffset(0)] public uint Value;

  // 下位ワード(オフセット0から2バイト分)を格納・参照するフィールド
  [FieldOffset(0)] public ushort LoWord;

  // 上位ワード(オフセット2から2バイト分)を格納・参照するフィールド
  [FieldOffset(2)] public ushort HiWord;
}

同様に、#pragma pack(n)のようなアラインメント(パッキングサイズ)を指定する命令も言語やコンパイラの機能としては用意されていませんが、StructLayout属性のPackフィールドによって指定することができます。

構造体のアラインメントの指定
using System;
using System.Runtime.InteropServices;

// 4バイトアラインメント
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct S {
  byte F1; // 0〜3バイト目の領域を専有するフィールド (格納・参照できるのは1バイト分)
  int  F2; // 4〜7バイト目の領域を専有するフィールド (格納・参照できるのは4バイト分)
}

また、StructLayout属性のSizeフィールドによってパッキングサイズとは個別に構造体のサイズを指定することができます。

構造体のサイズの指定
using System;
using System.Runtime.InteropServices;

// 構造体のサイズに2を指定
[StructLayout(LayoutKind.Sequential, Size = 2)]
struct S {
  byte F;
}

以降で個々の属性や指定できる値について詳しく解説します。



§2 フィールドのレイアウト (StructLayout属性)

StructLayout属性は、メモリ上でのフィールド(メンバ変数)の配置方法を指定するための属性です。 StructLayout属性は構造体だけでなくクラスにも適用することができます。 フィールドの配置方法はLayoutKind列挙体で指定することができ、次のいずれかを指定することが出来ます。

LayoutKind.Auto
ランタイムが自動的に最適な順序でフィールドを配置する (StructLayout属性を指定しない場合と同じ)
LayoutKind.Sequential
ランタイムによる自動的な並べ替えを行わず、コード上で記述されている順序のままフィールドを配置する
LayoutKind.Explicit
明示的に位置(オフセット)を指定してフィールドを配置する (各フィールドのオフセットを後述のFieldOffset属性で指定する)

構造体にStructLayout属性を適用する場合の例は次のようになります。

構造体にStructLayout属性を適用する例
using System;
using System.Runtime.InteropServices;

// フィールドのレイアウトを特に指定しない構造体
// (構造体内のフィールドはランタイムによって最適な位置に配置される
// そのため、フィールドの型や数によっては記述されている順序と実際の配置が異なる場合もありうる)
struct S1 {
  int F1;
  int F2;
}

// フィールドのレイアウトを特に指定しない構造体 (上記の構造体S1と同等)
[StructLayout(LayoutKind.Auto)]
struct S2 {
  int F1;
  int F2;
}

// StructLayoutにLayoutKind.Sequentialを指定した構造体
// (構造体内のフィールドは記述されたままの順序、つまりこの構造体S3ではF1→F2の順に配置される)
[StructLayout(LayoutKind.Sequential)]
struct S3 {
  int F1;
  int F2;
}

// StructLayoutにLayoutKind.Explicitを指定した構造体
// (構造体内のフィールドはFieldOffsetで指定されたオフセットに配置される
// この例ではF1は構造体の先頭から0バイト、F2は先頭から4バイトの位置に配置される)
[StructLayout(LayoutKind.Explicit)]
struct S4 {
  [FieldOffset(0)] int F1;
  [FieldOffset(4)] int F2;
}

LayoutKind.Explicitを指定した場合、すべてのフィールドに対してFieldOffset属性を指定する必要があります。 また、アンマネージAPI呼び出しの引数として渡される構造体(またはクラス)には、LayoutKind.SequentialまたはLayoutKind.Explicitのどちらかが指定されている必要があります。

LayoutKindのほか、StructLayout属性では構造体のアラインメントサイズを指定することもできます。

§3 フィールドのオフセット (FieldOffset属性)

FieldOffset属性は構造体(またはクラス)内における各フィールドの位置(オフセット)を指定するための属性です。 構造体にLayoutKind.Explicitを指定した場合にはすべてのフィールドに対してFieldOffset属性を指定し、オフセットを明示的に指定する必要があります。 この属性では、構造体の先頭からのオフセット値をバイト単位で指定します。

フィールドにFieldOffset属性を適用する例
using System;
using System.Runtime.InteropServices;

// フィールドにFieldOffsetを指定した構造体
// (FieldOffsetを指定する場合はLayoutKind.Explicitを指定する必要がある)
[StructLayout(LayoutKind.Explicit)]
struct S1 {
  [FieldOffset(0)] int F1; // このフィールドは構造体の先頭から0バイトの位置に配置される
  [FieldOffset(4)] int F2; // このフィールドは構造体の先頭から4バイトの位置に配置される
}

[StructLayout(LayoutKind.Explicit)]
struct S2 {
  // フィールド同士を不連続に配置する(フィールドから参照されない領域を作る)こともできる
  [FieldOffset(4)]  int F1;
  [FieldOffset(12)] int F2;
}

FieldOffset属性を指定している場合でも、フィールドの値の参照や設定は通常のフィールドと同じように行うことができます。

FieldOffset属性を適用した構造体を使用する例
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
struct S1 {
  [FieldOffset(0)] public int F1;
  [FieldOffset(4)] public int F2;
}

class Sample {
  static void Main()
  {
    S1 s = new S1();

    // 各フィールドに値を設定
    s.F1 = 0x00112233;
    s.F2 = 0x44556677;

    // 各フィールドの値を表示
    Console.WriteLine("s.F1 = 0x{0:X8}", s.F1);
    Console.WriteLine("s.F2 = 0x{0:X8}", s.F2);
  }
}
実行結果
s.F1 = 0x00112233
s.F2 = 0x44556677

この結果にもあるとおり通常の構造体の場合と何ら変わりありません。 しかし、メモリ上の配置は次の図のようになっているはずです。 (図はリトルエンディアン環境でのものです)

メモリ上での変数sと各フィールドの配置
オフセット
(バイト)
フィールド
0 F1 0x33
1 0x22
2 0x11
3 0x00
4 F2 0x77
5 0x66
6 0x55
7 0x44

§3.1 共用体の実装

FieldOffset属性では、他のフィールドと同じオフセットを指定することもできます。 つまり、複数のフィールドが同一のメモリ領域を参照するようにオフセットを指定することができます。 これにより、FieldOffset属性を使って共用体(union)と同じ構造を作ることができます。 C#やVBでは共用体を直接作成する言語機能はありませんが、構造体とFieldOffset属性を組み合わせることによって共用体となる構造体をつくることができます。

例として上位ワード(2バイト)と下位ワードを参照するフィールドと、ダブルワード(4バイト)を参照するフィールドを持つ共用体を作成すると次のようになります。

FieldOffset属性を使って共用体を作成する例
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
struct DWORD {
  // ダブルワード(オフセット0から4バイト分)を格納・参照するフィールド
  [FieldOffset(0)] public uint Value;

  // 下位ワード(オフセット0から2バイト分)を格納・参照するフィールド
  [FieldOffset(0)] public ushort LoWord;

  // 上位ワード(オフセット2から2バイト分)を格納・参照するフィールド
  [FieldOffset(2)] public ushort HiWord;
}

class Sample {
  static void Main()
  {
    DWORD dw = new DWORD();

    // ダブルワード値を設定
    dw.Value = 0x11223344;

    // 設定したダブルワード値を表示
    Console.WriteLine("Value = 0x{0:X8}", dw.Value);

    // 下位ワードと上位ワードの値を表示
    Console.WriteLine("LoWord = 0x{0:X4}", dw.LoWord);
    Console.WriteLine("HiWord = 0x{0:X4}", dw.HiWord);
  }
}
実行結果
Value = 0x11223344
LoWord = 0x3344
HiWord = 0x1122

実行結果からも共用体と同等の動作となっていることが分かります。 メモリ上の配置は次のようになっているはずです。

メモリ上での変数dwと各フィールドの配置
オフセット
(バイト)
フィールド
0 Value LoWord 0x44
1 0x33
2 HiWord 0x22
3 0x11

参考までに、これと同等の結果を得るためのC++コードを次に示します。

#include <iostream>

using namespace std;

union DWORD
{
  unsigned long Value;

  struct
  {
    unsigned short LoWord;
    unsigned short HiWord;
  } DWord;
};

int main()
{
  DWORD dw;

  dw.Value = 0x11223344;

  cout << "LoWord = 0x" << hex << dw.DWord.LoWord << endl;
  cout << "HiWord = 0x" << hex << dw.DWord.HiWord << endl;

  return 0;
}

§3.2 フィールドのオフセットの取得 (Marshal.OffsetOf)

C/C++のoffsetof()のように、フィールドのオフセットを取得したい場合はMarshal.OffsetOfメソッドを使用します。 このメソッドはFieldOffset属性で明示的にオフセットを指定していないフィールドでもオフセットを取得することができるため、実行時までオフセットがわからないフィールドのオフセットも取得することができます。

Marshal.OffsetOfメソッドではオフセットを取得したい型をType型、フィールド名を文字列で指定します。 オフセットはIntPtr型としてポインタの形で返されるため、数値として取得したい場合はさらにIntPtr.ToInt32メソッドを呼び出すなどして変換する必要があります。

フィールドのオフセットの取得
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
struct S1 {
  [FieldOffset(4)] int F1; // オフセットを4に設定したフィールド
}

[StructLayout(LayoutKind.Sequential)]
struct S2 {
  byte F1;
  int  F2; // 1バイトのフィールドの後ろに配置されるフィールド
}

// フィールドの型と数はS2と同じだが、アラインメントを1バイトに指定した構造体
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct S3 {
  byte F1;
  int  F2; // 1バイトのフィールドの後ろに配置されるフィールド
}

class Sample {
  static void Main()
  {
    // 構造体S1のフィールドF1のオフセットを取得する
    Console.WriteLine("OffsetOf(S1.F1) = {0}", Marshal.OffsetOf(typeof(S1), "F1").ToInt32());

    // 構造体S2のフィールドF2のオフセットを取得する
    Console.WriteLine("OffsetOf(S2.F2) = {0}", Marshal.OffsetOf(typeof(S2), "F2").ToInt32());

    // 構造体S3のフィールドF2のオフセットを取得する
    Console.WriteLine("OffsetOf(S3.F2) = {0}", Marshal.OffsetOf(typeof(S3), "F2").ToInt32());
  }
}
実行結果
OffsetOf(S1.F1) = 4
OffsetOf(S2.F2) = 4
OffsetOf(S3.F2) = 1

この結果からもわかるように、アラインメントによってフィールドのオフセットが異なる場合もあります。 アラインメントの指定については後述の§.アラインメントの指定 (StructLayoutAttribute.Pack)を参照してください。

.NET Framework 2.0以降では、非パブリックフィールドのオフセットも取得することができます。

.NET Framework 4.5.1以降では、オフセットを取得したい型(Type)を引数ではなく型パラメータとして指定することもできます。

型パラメータで型を指定してフィールドのオフセットを取得する
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
struct S1 {
  [FieldOffset(4)] int F1;
}

class Sample {
  static void Main()
  {
    // 構造体S1のフィールドF1のオフセットを取得する
    Console.WriteLine("OffsetOf(S1.F1) = {0}", Marshal.OffsetOf<S1>("F1").ToInt32());
  }
}
実行結果
OffsetOf(S1.F1) = 4

Marshal.OffsetOfメソッドではフィールド名を文字列で指定するため、取得したいフィールドの名前は既知である必要があります。 名前が未知の任意のフィールドについてオフセットを取得したい場合は、次の例のようにリフレクションによってフィールド名(FieldInfo.Name)を取得してからMarshal.OffsetOfメソッドを呼び出すようにします。

リフレクションを使用して任意のフィールドのオフセットを取得する
using System;
using System.Reflection;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
struct S {
  private byte F1;
  public short F2;
  public int F3;
}

class Sample {
  static void Main()
  {
    // 構造体Sのすべてのインスタンスフィールドのオフセットを取得して表示する
    Type t = typeof(S);

    foreach (FieldInfo f in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) {
      Console.WriteLine("OffsetOf(S.{0}) = {1}", f.Name, Marshal.OffsetOf(t, f.Name).ToInt32());
    }
  }
}
実行結果例
OffsetOf(S.F1) = 0
OffsetOf(S.F2) = 2
OffsetOf(S.F3) = 4

Type.GetFieldsメソッドについてはリフレクション §.メンバ情報の取得 (MemberInfo)を参照してください。

§4 アラインメントの指定 (StructLayoutAttribute.Pack)

構造体(またはクラス)のフィールドのアラインメント(パッキングサイズ)を変更するにはStructLayout属性でPackフィールドを指定します。 Packを指定した場合、各フィールドはPackで指定された値の倍数のオフセットに配置されます。 例えば2を指定すれば各フィールドのオフセットは2の倍数となります。 Packに指定できる値は2nである値のうち、0, 1, 2, 4, 8, 16, 32, 64, 128のいずれかです。 0を指定した場合は、デフォルトと同じ、つまりPackを指定しなかった場合と同じになります。

アラインメントの指定
using System;
using System.Runtime.InteropServices;

// デフォルトのアラインメント
struct S1 {
  byte F1;
  int  F2;
  int  F3;
}

// 1バイトアラインメント
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct S2 {
  byte F1;
  int  F2;
  int  F3;
}

// 2バイトアラインメント
[StructLayout(LayoutKind.Sequential, Pack = 2)]
struct S3 {
  byte F1;
  int  F2;
  int  F3;
}

// 4バイトアラインメント
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct S4 {
  byte F1;
  int  F2;
  int  F3;
}

class Sample {
  static void Main()
  {
    // 各構造体のフィールドのオフセットを表示する
    foreach (var type in new[] {typeof(S1), typeof(S2), typeof(S3), typeof(S4)}) {
      foreach (var field in new[] {"F1", "F2", "F3"}) {
        Console.WriteLine("OffsetOf({0}.{1}) = {2}", type, field, Marshal.OffsetOf(type, field).ToInt32());
      }
      Console.WriteLine();
    }
  }
}
実行結果
OffsetOf(S1.F1) = 0
OffsetOf(S1.F2) = 4
OffsetOf(S1.F3) = 8

OffsetOf(S2.F1) = 0
OffsetOf(S2.F2) = 1
OffsetOf(S2.F3) = 5

OffsetOf(S3.F1) = 0
OffsetOf(S3.F2) = 2
OffsetOf(S3.F3) = 6

OffsetOf(S4.F1) = 0
OffsetOf(S4.F2) = 4
OffsetOf(S4.F3) = 8

この結果からもわかるとおり、各フィールドは次のように配置されているはずです。

構造体S2(Pack=1)の各フィールドの配置
オフセット
(バイト)
フィールド
0 byte F1
1 int F2
2
3
4
5 int F3
6
7
8
構造体S3(Pack=2)の各フィールドの配置
オフセット
(バイト)
フィールド
0 byte F1
1 未使用
2 int F2
3
4
5
6 int F3
7
8
9
構造体S4(Pack=4)の各フィールドの配置
オフセット
(バイト)
フィールド
0 byte F1
1 未使用
2
3
4 int F2
5
6
7
8 int F3
9
10
11

なお、.NET FrameworkおよびMonoのデフォルトではアラインメントは4となるようです。

§5 サイズの指定 (StructLayoutAttribute.Size)

StructLayout属性でSizeフィールドを指定することにより、構造体のサイズを明示的に指定することができます。 例えば、サイズは固定されているが、具体的なフィールドは割り当てられていない構造体を作成したい場合などに使用します。 Packフィールドとは異なり、Sizeフィールドには2nだけでなく任意の値を指定することができます。

サイズの指定
using System;
using System.Runtime.InteropServices;

// デフォルトのサイズ
struct S1 {
  byte F1;
}

// 構造体のサイズに2を指定
[StructLayout(LayoutKind.Sequential, Size = 2)]
struct S2 {
  byte F1;
}

// 構造体のサイズに4を指定
[StructLayout(LayoutKind.Sequential, Size = 4)]
struct S3 {
  byte F1;
}

// 構造体のサイズに6を指定
[StructLayout(LayoutKind.Sequential, Size = 6)]
struct S4 {
  byte F1;
}

class Sample {
  static void Main()
  {
    // 各構造体のサイズを表示する
    foreach (var type in new[] {typeof(S1), typeof(S2), typeof(S3), typeof(S4)}) {
      Console.WriteLine("SizeOf({0}) = {1}", type, Marshal.SizeOf(type));
    }
  }
}
実行結果
SizeOf(S1) = 1
SizeOf(S2) = 2
SizeOf(S3) = 4
SizeOf(S4) = 6

構造体のサイズの取得については構造体のサイズも参照してください。

構造体内で定義されているフィールドの総サイズがSizeフィールドで指定している値を超える場合、構造体のサイズは当然Sizeフィールドで指定している値よりも大きくなります。 コンパイルエラーや実行時エラーは発生しないため、意図しない動作の原因となりうることに注意が必要です。

using System;
using System.Runtime.InteropServices;

// 構造体のサイズに2を指定
[StructLayout(LayoutKind.Sequential, Size = 2)]
struct S {
  // 4バイト分のフィールドを定義
  int F;
}

class Sample {
  static void Main()
  {
    Console.WriteLine("SizeOf(S) = {0}", Marshal.SizeOf(typeof(S)));
  }
}
実行結果
SizeOf(S) = 4