C/C++にあるunionをC#で使用する方法は、ネットにいくつか解説しているサイトがあります。しかし、構造体がunionになったものは例があまりないため紹介したいと思います。
基本的に、StructLayout = "LayoutKind.Explicit"(明示的にレイアウトを指定する)とFieldOffsetの組み合わせで実現できます。
[StructLayout(LayoutKind.Explicit)] public struct Parent { [FieldOffset(0)] public int A; [FieldOffset(4)] public UnionStructure_1 B; // BとCで構造体が重なっている [FieldOffset(4)] public UnionStructure_2 C; } [StructLayout(LayoutKind.Explicit)] public struct UnionStructure_1 { [FieldOffset(0)] public int BA; // Offsetは0から始める [FieldOffset(4)] public int BB; } [StructLayout(LayoutKind.Explicit)] public struct UnionStructure_2 { [FieldOffset(0)] public byte CA; // UnionStructure_1の(0)~と同じ場所になる [FieldOffset(1)] public byte CB; [FieldOffset(2)] public byte CC; [FieldOffset(3)] public byte CD; [FieldOffset(4)] public int CE; } // // 対応するC/C++コード // typedef struct { // int a; // union { // struct { // int ba; // long bb; // } B; // struct { // byte ca; // byte cb; // byte cc; // byte cd; // int ce; // } C; // } // } Parent;
上記に対応するC/C++のコード
Explicitの時はByValArrayを指定できない
えー、、、これ本当に?別にいいと思うんだけど…
StructLayoutをLayoutKind.Explicitにした場合、ByValArrayのメンバーを指定すると実行時に以下のエラーが出て指定できません。
System.TypeLoadException: 'アセンブリ '{$アセンブリ名}, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' からの型 '{$名前空間}.{$クラス名}' を読み込めませんでした。 オフセット 4 に不適切に整列されたか、 オブジェクト以外のフィールドでオーバーラップされたオブジェクト フィールドが含まれています。'
具体的には以下コードになります。
[StructLayout(LayoutKind.Explicit)] public ExplicitClass { [FieldOffset(0)] public int FirstMember; [FieldOffset(4)] public int SecondMember; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] // ★★★指定できない [FieldOffset(4)] public byte[] SecondMemberByteArray; }
回避手段は2つあります。
まずは1つ目。unsafeステートメントと固定長配列指定 = fixedキーワードを組み合わせる。
この場合、コンパイルするときにプロジェクトに「アンセーフ コードの許可」をする必要があります。
[StructLayout(LayoutKind.Explicit)] public unsafe struct ExplicitClass // unsafe指定を追加 { [FieldOffset(0)] public int FirstMember; [FieldOffset(4)] public int SecondMember; //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] // ★★★指定できない //[FieldOffset(4)] public byte[] SecondMemberByteArray; public fixed byte SecondMemberByteArray[4]; // fixedキーワードで固定長配列を宣言 }
2つめは、やや力業です。バイト配列をバイトにバラしてしまいます。各メンバーにFieldOffsetを指定すれば問題は起きません。
[StructLayout(LayoutKind.Explicit)] public struct ExplicitClass { [FieldOffset(0)] public int FirstMember; [FieldOffset(4)] public int SecondMember; //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] // ★★★指定できない //[FieldOffset(4)] public byte[] SecondMemberByteArray; // [FieldOffset(4)] public byte SecondMemberByte_1; [FieldOffset(5)] public byte SecondMemberByte_2; [FieldOffset(6)] public byte SecondMemberByte_3; [FieldOffset(7)] public byte SecondMemberByte_4; }
よくある間違い
余談ですが以下メッセージは、コーディング間違えの可能性が高いです。よく自分の実装を見ましょう。
System.ArgumentException: '型 '{$名前空間}.{$クラス名}' はアンマネージ構造体としてマーシャリングできません。 有効なサイズ、またはオフセットの計算ができません。'
ほんの一例ですが、以下のように属性と宣言でサイズが異なる場合に出たりします。
[StructLayout(LayoutKind.Explicit)] public ExplicitClass { [FieldOffset(0)] public int FirstMember; [FieldOffset(4)] public int SecondMember; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] [FieldOffset(4)] public byte SecondMemberByteArray; // byteの後ろに[]が抜けている!! }