バイナリデータのデータ構造


アジェンダ

はじめに

ゲームやアプリケーションでは、往々にして独自のバイナリデータを作成することがあります。
なぜならテキストデータよりも基本的にファイルサイズが小さくなり、処理が高速だからです。
本記事では、バイナリデータのデータ構造に関するプラクティスを紹介します。

バイナリデータのデータ構造

バイナリデータは以下のようなデータ構造にすると良いです。

/// <summary>
/// binファイルヘッダ
/// </summary>
public struct BinFileHeaderStruct
{
    public uint signature;  //!<シグネチャ [4]
    public byte byteOrder;  //!<バイトオーダー [5]
    public byte bitOrder;   //!<ビットオーダー [6]
    public byte majorVersion;   //!<メジャーバージョン [7]
    public byte minorVersion;   //!<マイナーバージョン [8]
    public uint fileHeaderStructSize;   //!<ファイルヘッダ構造体サイズ [byte] [12]
    public uint keyDataStructSize;   //!<キーデータ構造体サイズ [byte] [16]
    public uint keyDataNum;  //!<キーデータ数 [20]
};

/// <summary>
/// binファイルキーデータ
/// </summary>
public struct BinFileKeyDataStruct
{
    public int key;  //!<キー [4]
};

シグネチャ

ファイルの識別子です。
「独自のバイナリデータは先頭の4バイトにシグネチャを入れる」というフォーマットで運用することで、不正なファイルの読み込みを防止できます。
識別子はユニークなものであれば何でも良いですが、筆者はだいたいそのデータの表す4文字に設定しています。
例えば、アクションデータであれば"ACTN"、エフェクトデータであれば"EFCT"などです。
また、コメントにバイト数を書きましょう。
デバッグがしやすくなります。

バイトオーダー、ビットオーダー

異なるプラットフォームでリリースする可能性がある場合は、バイトオーダーとビットオーダーの情報を保持しましょう。
設定する数値は0, 1をリトルエンディアン、ビッグエンディアンに対応させても良いですが、筆者はわかりやすさのため0x4c(L)、0x42(B)を使用しています。
ただし、近年ではほとんどのケースでバイトオーダーがリトルエンディアン、ビットオーダーがビッグエンディアンなので、必要ないと判断した場合は省略しても良いでしょう。

メジャーバージョン、マイナーバージョン

互換性を保つためにバージョンの情報を保持しましょう。
筆者はよくメジャーバージョン、マイナーバージョンの2つにしていますが、粒度はお好みで。

ファイルヘッダ構造体サイズ、キーデータ構造体サイズ、キーデータ数

ファイルヘッダ構造体サイズ、データ部の構造体サイズを保持します。
データが複数ある場合は、データ数を保持します。
この三つがあれば、先頭アドレスがわかっていれば、どのデータに対してもオフセットの計算のみでアクセスできます。
また、データに保持することで、互換性を保った実装がシンプルになります。
ただし、C#だとbyte型のポインタを持っておくのは扱いづらいため、恩恵は少なくなります。

バイナリデータの読み込み

C#では、バイナリデータの読み込みには、以下のような拡張メソッドを用意すると良いです。

/// <summary>
/// BinaryReader拡張メソッド
/// </summary>
public static unsafe class BinaryReaderExtensions
{
    /// <summary>
    /// 構造体の読み込み
    /// </summary>
    /// <typeparam name="TStruct">構造体</typeparam>
    /// <param name="binaryReader">BinaryReader</param>
    /// <returns>構造体</returns>
    public static TStruct ReadStruct<TStruct>(this BinaryReader binaryReader) where TStruct : struct
    {
        return binaryReader.ReadStruct<TStruct>(Marshal.SizeOf(typeof(TStruct)));
    }

    /// <summary>
    /// 構造体の読み込み
    /// </summary>
    /// <typeparam name="TStruct">構造体</typeparam>
    /// <param name="binaryReader">BinaryReader</param>
    /// <param name="size">サイズ</param>
    /// <returns>構造体</returns>
    public static TStruct ReadStruct<TStruct>(this BinaryReader binaryReader, int size) where TStruct : struct
    {
        var buffer = binaryReader.ReadBytes(size);
        var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);

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

以下のようなコードで読み込みます。

var fileHeader = binaryReader.ReadStruct<BinFileHeaderStruct>();
binaryReader.BaseStream.Position = fileHeader.fileHeaderStructSize;
var dataArray = new BinFileKeyDataStruct[fileHeader.keyDataNum];
for (int idx = 0; idx < fileHeader.keyDataNum; idx++)
{
    dataArray[idx] = binaryReader.ReadStruct<BinFileKeyDataStruct>((int)fileHeader.keyDataStructSize);
}

まとめ

  • ヘッダのメンバ

    • シグネチャ
    • バイトオーダー、ビットオーダー
    • メジャーバージョン、マイナーバージョン
    • ファイルヘッダ構造体サイズ、データ部構造体サイズ、データ数 (データが複数ある場合)
  • バイナリデータ自身の情報でどのデータに対してもオフセットの計算のみでアクセスできるようにする
  • コメントにバイト数を書く

参考文献

© 2020 Manicreator