C#で構造体のunion(共用体)を扱う

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の後ろに[]が抜けている!!
}