はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】メモリレイアウトを制御できるStructLayout &LayoutKind.Explicitを用いて別の型として解釈する(あとUnsafe.Asを利用した例も)

はじめに

今回はStructLayoutLayoutKind.Explicitを用いて指定したメモリ領域を別の型として解釈する方法について紹介したいと思います。

別の型として解釈する例(bool + byte + ushort => int)

別の型として解釈する

StructLayoutAttributeLayoutKind.Explicitを上手に組み合わせることで、本来はキャストできないような別な型として解釈させることができます。

// サンプル : bool(1byte) + byte(1byte) + ushort(2byte) => int(4byte)
[StructLayout(LayoutKind.Explicit)]
public struct SampleStruct
{
    [FieldOffset(0)]
    public bool A;

    [FieldOffset(1)]
    public byte B;

    [FieldOffset(2)]
    public ushort C;

    [FieldOffset(0)] 
    public int Value;
}

public static class Program
{
    public static void Main(string[] args)
    {
        var sample = new SampleStruct()
        {
            A = true,               // 1
            B = 255,                // 255
            C = 1000,               // 232, 3
        };
        
        // 65601281
        Console.WriteLine(sample.Value);
        
        // 1, 255, 232, 2
        Console.WriteLine(string.Join(",", BitConverter.GetBytes(sample.Value)));
    }
}
別の型として解釈する例(bool + byte + ushort => int)

StructLayoutとは

そもそもStructLayoutとは何だろうということで概要を載せておきます。

メモリ内のクラスまたは構造体のデータ フィールドの物理的なレイアウトを制御できます。

StructLayoutAttribute クラス (System.Runtime.InteropServices) | Microsoft Learn

namespace System.Runtime.InteropServices
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)]
    public sealed class StructLayoutAttribute : Attribute
    {
        public StructLayoutAttribute(LayoutKind layoutKind)
        {
            Value = layoutKind;
        }

        public StructLayoutAttribute(short layoutKind)
        {
            Value = (LayoutKind)layoutKind;
        }

        public LayoutKind Value { get; }

        public int Pack;
        public int Size;
        public CharSet CharSet;
    }
}

メモリレイアウト方式を指定できるというわけですね。引数にはLayoutKindを取ります。

namespace System.Runtime.InteropServices
{
    // Used in the StructLayoutAttribute class
    public enum LayoutKind
    {
        Sequential = 0,
        Explicit = 2,
        Auto = 3,
    }
}
  • Auto(3):アンマネージ メモリ内のオブジェクトのメンバーに対して適切なレイアウトを自動的に選択します。
  • Explicit(2):各メンバーはFieldOffsetAttributeを使用して、その型内のフィールドの位置を指定する必要があります。
  • Sequential(0):アンマネージ メモリにエクスポートするときに表示される順番に従ってレイアウトされます。 メンバーは、Packで指定したパッキングに従ってレイアウトされます。

詳細は未確認飛行さんの記事がわかりやすかったです。

ufcpp.net

Unsafe.As

またUnsafe.Asを利用することで、指定したメモリ領域を別の型として解釈することができます。

public static ref TTo As<TFrom,TTo> (ref TFrom source);

指定したマネージド ポインターを、 型の値への新しいマネージド ポインターとして再解釈します。

learn.microsoft.com

// Sample型のオブジェクトをSample2型として解釈するサンプル
[StructLayout(LayoutKind.Explicit)]
public struct Sample
{
    [FieldOffset(0)]
    public bool A;

    [FieldOffset(1)]
    public byte B;

    [FieldOffset(2)]
    public ushort C;
}

public struct Sample2
{
    public int Value;
}

public static class Program
{
    public static void Main(string[] args)
    {
        var sample = new Sample()
        {
            A = true,               // 1
            B = 255,                // 255
            C = 1000,               // 232, 3
        };

        var sample2 = Unsafe.As<Sample, Sample2>(ref sample);

        // 65601281
        Console.WriteLine(sample2.Value);
        
        // 1, 255, 232, 3
        Console.WriteLine(string.Join(",", BitConverter.GetBytes(sample2.Value)));
    }
}