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

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

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);
    }
  }
}
Imports System
Imports System.IO
Imports System.Runtime.InteropServices

''' <summary>
''' ポインタを得るためにMarshal.AllocHGlobalでバイト配列のコピー用領域を作成し、
''' Marshal.PtrToStructure・Marshal.StructureToPtrで変換して構造体の読み書きを行う
''' </summary>
''' <remarks>参照型のフィールドを持つ構造体は読み書きできない</remarks>
Module ReadWriteStructWithAllocHGlobal
  Public Sub WriteTo(Of TStruct As Structure)(ByVal writer As BinaryWriter, ByVal s As TStruct)
    Dim size As Integer = Marshal.SizeOf(GetType(TStruct))
    Dim buffer As Byte() = New Byte(size - 1) {}
    Dim ptr As IntPtr = IntPtr.Zero 

    Try
      ptr = Marshal.AllocHGlobal(size)

      Marshal.StructureToPtr(s, ptr, false)

      Marshal.Copy(ptr, buffer, 0, size)
    Finally
      If ptr <> IntPtr.Zero Then Marshal.FreeHGlobal(ptr)
    End Try

    writer.Write(buffer)
  End Sub

  Public Function ReadFrom(Of TStruct As Structure)(ByVal reader As BinaryReader) As TStruct
    Dim size As Integer = Marshal.SizeOf(GetType(TStruct))
    Dim ptr As IntPtr = IntPtr.Zero 

    Try
      ptr = Marshal.AllocHGlobal(size)

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

      Return DirectCast(Marshal.PtrToStructure(ptr, GetType(TStruct)), TStruct)
    Finally
      If ptr <> IntPtr.Zero Then Marshal.FreeHGlobal(ptr)
    End Try
  End Function
End Module

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();
    }
  }
}
Imports System
Imports System.IO
Imports System.Runtime.InteropServices

''' <summary>
''' ポインタを得るためにGCHandle.Allocでバイト配列のピニングを行い、
''' Marshal.PtrToStructure・Marshal.StructureToPtrで変換して構造体の読み書きを行う
''' </summary>
''' <remarks>参照型のフィールドを持つ構造体は読み書きできない</remarks>
Module ReadWriteStructWithAllocGCHandle
  Public Sub WriteTo(Of TStruct As Structure)(ByVal writer As BinaryWriter, ByVal s As TStruct)
    Dim buffer As Byte() = New Byte(Marshal.SizeOf(GetType(TStruct)) - 1) {}
    Dim handle As GCHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned)

    Try
      Marshal.StructureToPtr(s, handle.AddrOfPinnedObject(), false)
    Finally
      handle.Free()
    End Try

    writer.Write(buffer)
  End Sub

  Public Function ReadFrom(Of TStruct As Structure)(ByVal reader As BinaryReader) As TStruct
    Dim buffer As Byte() = reader.ReadBytes(Marshal.SizeOf(GetType(TStruct)))
    Dim handle As GCHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned)

    Try
      Return DirectCast(Marshal.PtrToStructure(handle.AddrOfPinnedObject(), GetType(TStruct)), TStruct)
    Finally
      handle.Free()
    End Try
  End Function
End Module

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

この方法では、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);
    }
  }
}

(VBではポインタが使用できないためこの方法は使えない)

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);
  }
}
Imports System
Imports System.IO
Imports System.Runtime.Serialization.Formatters.Binary

''' <summary>BianryFormatterを使って構造体の読み書きを行う</summary>
''' <remarks>
''' メモリ上のバイト表現とは異なるバイト表現で読み書きされる。
''' このメソッドで読み書きする構造体にはSerializableAttributeが付与されている必要がある。
''' </remarks>
Module ReadWriteStructWithBinaryFormatter
  Public Sub WriteTo(Of TStruct As Structure)(ByVal writer As BinaryWriter, ByVal s As TStruct)
    Dim formatter As New BinaryFormatter()

    formatter.Serialize(writer.BaseStream, s)
  End Sub

  Public Function ReadFrom(Of TStruct As Structure)(ByVal reader As BinaryReader) As TStruct
    Dim formatter As New BinaryFormatter()

    Return DirectCast(formatter.Deserialize(reader.BaseStream), TStruct)
  End Function
End Module

リフレクション

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

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
  }
}
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Linq
Imports System.Runtime.InteropServices
Imports System.Reflection

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

    ' TStructの全フィールドを取得して、フィールドのオフセット順に並べ替え
    Dim fields As IEnumerable(Of FieldInfo) = _
      type.GetFields(BindingFlags.Instance Or BindingFlags.NonPublic Or BindingFlags.Public) _
          .OrderBy(Function (f As FieldInfo) Marshal.OffsetOf(type, f.Name).ToInt64())

    For Each field As FieldInfo In Fields
      If field.FieldType = GetType(Byte) Then
        ' フィールドがByteなら、そのまま書き込む
        writer.Write(CByte(field.GetValue(s)))
      Else
        ' フィールドがByte以外なら、BitConverter.GetBytesメソッドでByte()に変換して書き込む
        Dim getBytes As MethodInfo = GetType(BitConverter).GetMethod("GetBytes", New Type() {field.FieldType})

        If getBytes Is Nothing Then Throw New NotSupportedException("unsupported field type: " + field.FieldType.FullName)

        writer.Write(DirectCast(getBytes.Invoke(Nothing, New Object() {field.GetValue(s)}), Byte()))
      End If
    Next
  End Sub

  Public Function ReadFrom(Of TStruct As Structure)(ByVal reader As BinaryReader) As TStruct
    Dim type As Type = GetType(TStruct)
    Dim ret As New TStruct

    ' VBでボックス化相当の処理を行う
    Dim retValue As ValueType = ret

    ' TStructの全フィールドを取得して、フィールドのオフセット順に並べ替え
    Dim fields As IEnumerable(Of FieldInfo) = _
      type.GetFields(BindingFlags.Instance Or BindingFlags.NonPublic Or BindingFlags.Public) _
          .OrderBy(Function (f As FieldInfo) Marshal.OffsetOf(type, f.Name).ToInt64())

    For Each field As FieldInfo In Fields
      If field.FieldType = GetType(Byte) Then
        ' フィールドがByteなら読み込んだ値をそのままフィールドの値とする
        field.SetValue(retValue, reader.ReadByte())
      Else
        ' フィールドがByte以外なら、BitConverter.ToXXXメソッドで該当する型に変換した値をフィールドの値とする
        Dim toXXX As MethodInfo = _
          GetType(BitConverter).GetMethods(BindingFlags.Static Or BindingFlags.Public) _
                               .Where(Function(m As MethodInfo) m.Name.StartsWith("To") AndAlso m.ReturnType = field.FieldType) _
                               .FirstOrDefault()

        If toXXX Is Nothing Then Throw New NotSupportedException("unsupported field type: " + field.FieldType.FullName)

        Dim size As Integer = Marshal.SizeOf(field.FieldType)

        field.SetValue(retValue, toXXX.Invoke(Nothing, New Object() {reader.ReadBytes(size), 0}))
      End If
    Next

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

使用例

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

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);
    }
  }
}
Imports System
Imports System.IO
Imports System.Runtime.InteropServices

Class Sample
  ' 読み書きを行う構造体
  <StructLayout(LayoutKind.Sequential)> _
  Structure MyDate
    Public Year As Short
    Public Month As Byte
    Public Day As Byte
  End Structure

  Shared Sub Main()
    ' ファイルdump.binに構造体の内容を書き出す
    Using writer As New BinaryWriter(File.OpenWrite("dump.bin"))
      Dim d As New MyDate()

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

      ReadWriteStructWithAllocGCHandle.WriteTo(writer, d)
    End Using

    ' ファイルdump.binから構造体の内容を読み込む
    Using reader As New BinaryReader(File.OpenRead("dump.bin"))
      Dim d As MyDate= ReadWriteStructWithAllocGCHandle.ReadFrom(Of MyDate)(reader)

      Console.WriteLine("{0}-{1}-{2}", d.Year, d.Month, d.Day)
    End Using
  End Sub
End Class