構造体とバイト配列の相互変換を行う方法、および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でシリアライズすることで構造体のバイト表現を取得する。

この例で使用しているBinaryFormatter.Serialize/Deserializeについて、.NET 5以降においては使用は推奨されず、できるだけ早く使用をやめる必要があるとされています。 以下のコードでは、これを示すコンパイル時警告SYSLIB0011が出力されます。 特にASP.NET 5.0以降では、明示的にBinaryFormatterの使用を有効にしない限り常に例外NotSupportedExceptionがスローされます

BinaryFormatter 型は危険であり、データ処理用としては "推奨されません"。 アプリケーションでは、処理するデータが信頼できると思われる場合でも、できるだけ早く BinaryFormatter の使用をやめる必要があります。 BinaryFormatter は安全ではなく、セキュリティで保護することはできません。

BinaryFormatter セキュリティ ガイド | Microsoft Docs

BinaryFormatter シリアル化メソッドが古い形式になり、ASP.NET アプリでは使用不可に

BinaryFormatter、Formatter、および IFormatter の Serialize と Deserialize のメソッドが古いと見なされ、警告が示されるようになりました。 また、ASP.NET アプリでは、BinaryFormatter のシリアル化が既定で禁止されます。

変更の説明

BinaryFormatter のセキュリティ脆弱性により、次のメソッドは古いと見なされ、ID SYSLIB0011 のコンパイル時警告が生成されるようになりました。 また、ASP.NET Core 5.0 以降のアプリでは、Web アプリによって BinaryFormatter 機能が再有効化されていない限り、NotSupportedException がスローされます。

  • BinaryFormatter.Serialize
  • BinaryFormatter.Deserialize
基本クラス ライブラリの破壊的変更 - .NET Core | Microsoft Docs

これに従い、シリアライズによる構造体とバイト配列の相互変換は、ASP.NET 5.0以降では明示的に有効にしない限り使用できない手段で、またそれ以外のフレームワークでも推奨できる手段ではなくなっています。

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

/// <summary>BianryFormatterを使って構造体の読み書きを行う</summary>
/// <remarks>
/// メモリ上のバイト表現とは異なるバイト表現で読み書きされる。
/// このメソッドで読み書きする構造体にはSerializableAttributeが付与されている必要がある。
/// ⚠.NET 5以降では警告SYSLIB0011となる
/// ⚠ASP.NET 5.0以降では警告SYSLIB0011となり、BinaryFormatterの使用を明示的に有効にしない限り実行時に例外NotSupportedExceptionがスローされる
/// </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); // [.NET 5] warning SYSLIB0011: 'BinaryFormatter.Serialize(Stream, object)' は旧形式です ('BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.')
  }

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

    return (TStruct)formatter.Deserialize(reader.BaseStream); // [.NET 5] warning SYSLIB0011: 'BinaryFormatter.Deserialize(Stream)' は旧形式です ('BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.')
  }
}
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) ' [.NET 5] warning SYSLIB0011: 'Public Overloads Sub Serialize(serializationStream As Stream, graph As Object)' は廃止されています: 'BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.'。
  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) ' [.NET 5] warning SYSLIB0011: 'Public Overloads Function Deserialize(serializationStream As Stream) As Object' は廃止されています: 'BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.'。
  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