構造体とバイト配列の相互変換を行う方法、およびBinaryReader・BinaryWriterで構造体の読み書きを行う方法について。

.NET Frameworkでは任意の構造体とバイト配列を相互に変換するクラスやメソッドが用意されていない。 また、StreamBinaryReaderBinaryWriterなどのクラスも直接構造体の読み書きを行う方法をサポートしていない。 そのため、以下で紹介するような方法を使って独自に実装する必要がある。

§1 Marshal.AllocHGlobal + Marshal.StructureToPtr/PtrToStructure

この方法では、Marshal.AllocHGlobalを使ってバイト配列の読み書きを行う領域を作成し、Marshal.Copyで構造体のバイト表現をコピーする。 コピーに際して、Marshal.StructureToPtrおよびMarshal.PtrToStructureで構造体とポインタを相互に変換する。

using System;
using System.IO;
using System.Runtime.InteropServices;

/// <summary>
/// ポインタを得るためにMarshal.AllocHGlobalでバイト配列のコピー用領域を作成し、
/// Marshal.PtrToStructure・Marshal.StructureToPtrで変換して構造体の読み書きを行う
/// </summary>
/// <remarks>参照型のフィールドを持つ構造体は読み書きできない</remarks>
static class ReadWriteStructWithAllocHGlobal {
  public static void WriteTo<TStruct>(BinaryWriter writer, TStruct s) where TStruct : struct
  {
    var size = Marshal.SizeOf(typeof(TStruct));
    var buffer = new byte[size];
    var ptr = IntPtr.Zero;

    try {
      ptr = Marshal.AllocHGlobal(size);

      Marshal.StructureToPtr(s, ptr, false);

      Marshal.Copy(ptr, buffer, 0, size);
    }
    finally {
      if (ptr != IntPtr.Zero)
        Marshal.FreeHGlobal(ptr);
    }

    writer.Write(buffer);
  }

  public static TStruct ReadFrom<TStruct>(BinaryReader reader) where TStruct : struct
  {
    var size = Marshal.SizeOf(typeof(TStruct));
    var ptr = IntPtr.Zero;

    try {
      ptr = Marshal.AllocHGlobal(size);

      Marshal.Copy(reader.ReadBytes(size), 0, ptr, size);

      return (TStruct)Marshal.PtrToStructure(ptr, typeof(TStruct));
    }
    finally {
      if (ptr != IntPtr.Zero)
        Marshal.FreeHGlobal(ptr);
    }
  }
}

§2 Marshal.GCAlloc + Marshal.StructureToPtr/PtrToStructure

この方法では、Marshal.AllocHGlobalを使うかわりにGCHandle.Allocを使うことで直接バイト配列のポインタを取得し、Marshal.PtrToStructureMarshal.StructureToPtrで構造体に変換する。

using System;
using System.IO;
using System.Runtime.InteropServices;

/// <summary>
/// ポインタを得るためにGCHandle.Allocでバイト配列のピニングを行い、
/// Marshal.PtrToStructure・Marshal.StructureToPtrで変換して構造体の読み書きを行う
/// </summary>
/// <remarks>参照型のフィールドを持つ構造体は読み書きできない</remarks>
static class ReadWriteStructWithAllocGCHandle {
  public static void WriteTo<TStruct>(BinaryWriter writer, TStruct s) where TStruct : struct
  {
    var buffer = new byte[Marshal.SizeOf(typeof(TStruct))];
    var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);

    try {
      Marshal.StructureToPtr(s, handle.AddrOfPinnedObject(), false);
    }
    finally {
      handle.Free();
    }

    writer.Write(buffer);
  }

  public static TStruct ReadFrom<TStruct>(BinaryReader reader) where TStruct : struct
  {
    var buffer = reader.ReadBytes(Marshal.SizeOf(typeof(TStruct)));
    var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);

    try {
      return (TStruct)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(TStruct));
    }
    finally {
      handle.Free();
    }
  }
}

§3 ポインタのキャスト・代入

この方法では、Marshalクラスのメソッドは使わず、直接ポインタを操作してバイト配列と構造体の変換を行う。

using System;
using System.IO;
using System.Runtime.InteropServices;

/// <summary>
/// fixedステートメントでポインタを取得し、
/// ポインタのキャスト・代入により構造体の読み書きを行う
/// </summary>
/// <remarks>参照型のフィールドを持つ構造体は読み書きできない</remarks>
static unsafe class ReadWriteStructWithFixedPointer {
  public static void WriteTo<TStruct>(BinaryWriter writer, TStruct s) where TStruct : struct
  {
    var buffer = new byte[Marshal.SizeOf(typeof(TStruct))];

    fixed (byte* ptr = buffer) {
      *((TStruct*)ptr) = s;
    }

    writer.Write(buffer);
  }

  public static TStruct ReadFrom<TStruct>(BinaryReader reader) where TStruct : struct
  {
    var buffer = reader.ReadBytes(Marshal.SizeOf(typeof(TStruct)));

    fixed (byte* ptr = buffer) {
      return *((TStruct*)ptr);
    }
  }
}

§4 BinaryFormatterによるシリアライズ・デシリアライズ

この方法では、BianryFormatterでシリアライズすることで構造体のバイト表現を取得する。

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

/// <summary>BianryFormatterを使って構造体の読み書きを行う</summary>
/// <remarks>
/// メモリ上のバイト表現とは異なるバイト表現で読み書きされる。
/// このメソッドで読み書きする構造体にはSerializableAttributeが付与されている必要がある。
/// </remarks>
static class ReadWriteStructWithBinaryFormatter {
  public static void WriteTo<TStruct>(BinaryWriter writer, TStruct s) where TStruct : struct
  {
    var formatter = new BinaryFormatter();

    formatter.Serialize(writer.BaseStream, s);
  }

  public static TStruct ReadFrom<TStruct>(BinaryReader reader) where TStruct : struct
  {
    var formatter = new BinaryFormatter();

    return (TStruct)formatter.Deserialize(reader.BaseStream);
  }
}

§5 リフレクション

この方法では、構造体のバイト表現を直接取得するのではなく、構造体のフィールドをリフレクションによって取得し、フィールドに格納されている値を個別にバイト配列へと変換していく。 つまり、構造体の全フィールドを一つずつ読み書きする方法と同じで、それをリフレクションによって汎用化している。

using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Reflection;

/// <summary>リフレクションとBitConverterクラスのバイト配列変換メソッドを使って構造体の読み書きを行う</summary>
/// <exception>フィールドの型がBitConverterでは変換できない型の場合はNotSupportedException</exception>
/// <remarks>
/// この実装は不完全で、フィールドが構造体の場合は再帰的に呼び出しを行うなどの処理を追加する必要がある。
/// </remarks>
static class ReadWriteStructWithReflection {
  public static void WriteTo<TStruct>(BinaryWriter writer, TStruct s) where TStruct : struct
  {
    var type = s.GetType();

    // TStructの全フィールドを取得して、フィールドのオフセット順に並べ替え
    var fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
                     .OrderBy(f => Marshal.OffsetOf(type, f.Name).ToInt64());

    foreach (var field in fields) {
      if (field.FieldType == typeof(byte)) {
        // フィールドがbyteなら、そのまま書き込む
        writer.Write((byte)field.GetValue(s));
      }
      else {
        // フィールドがbyte以外なら、BitConverter.GetBytesメソッドでbyte[]に変換して書き込む
        var getBytes = typeof(BitConverter).GetMethod("GetBytes", new[] {field.FieldType});

        if (getBytes == null)
          throw new NotSupportedException("unsupported field type: " + field.FieldType.FullName);

        writer.Write((byte[])getBytes.Invoke(null, new[] {field.GetValue(s)}));
      }
    }
  }

  public static TStruct ReadFrom<TStruct>(BinaryReader reader) where TStruct : struct
  {
    var type = typeof(TStruct);
    var ret = default(TStruct);
    object retBoxed = ret; // boxing

    // TStructの全フィールドを取得して、フィールドのオフセット順に並べ替え
    var fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
                     .OrderBy(f => Marshal.OffsetOf(type, f.Name).ToInt64());

    foreach (var field in fields) {
      if (field.FieldType == typeof(byte)) {
        // フィールドがbyteなら読み込んだ値をそのままフィールドの値とする
        field.SetValue(retBoxed, reader.ReadByte());
      }
      else {
        // フィールドがbyte以外なら、BitConverter.ToXXXメソッドで該当する型に変換した値をフィールドの値とする
        var toXXX = typeof(BitConverter).GetMethods(BindingFlags.Static | BindingFlags.Public)
                                        .Where(m => m.Name.StartsWith("To") && m.ReturnType == field.FieldType)
                                        .FirstOrDefault();

        if (toXXX == null)
          throw new NotSupportedException("unsupported field type: " + field.FieldType.FullName);

        var size = Marshal.SizeOf(field.FieldType);

        field.SetValue(retBoxed, toXXX.Invoke(null, new object[] {reader.ReadBytes(size), 0}));
      }
    }

    return (TStruct)retBoxed; // unboxing
  }
}
ReadWriteStructWithReflection.cs (構造体からバイト配列を出力できたらどれだけ楽だろうか - test)
上記サンプルを拡張して、配列フィールド・文字列フィールド・入れ子になった構造体フィールドの書き込み、エンディアンを指定した書き込みをサポートしたもの

§6 使用例

ここまでで紹介した方法を実際に使用する例。

using System;
using System.IO;
using System.Runtime.InteropServices;

class Sample {
  // 読み書きを行う構造体
  [StructLayout(LayoutKind.Sequential)]
  struct Date {
    public short Year;
    public byte Month;
    public byte Day;
  }

  static void Main()
  {
    // ファイルdump.binに構造体の内容を書き出す
    using (var writer = new BinaryWriter(File.OpenWrite("dump.bin"))) {
      var d = new Date();

      d.Year = 2013;
      d.Month = 4;
      d.Day = 1;

      ReadWriteStructWithAllocGCHandle.WriteTo(writer, d);
    }

    // ファイルdump.binから構造体の内容を読み込む
    using (var reader = new BinaryReader(File.OpenRead("dump.bin"))) {
      var d = ReadWriteStructWithAllocGCHandle.ReadFrom<Date>(reader);

      Console.WriteLine("{0}-{1}-{2}", d.Year, d.Month, d.Day);
    }
  }
}