エンディアンを指定できるBitConverter

バイト配列から任意のプリミティブ型(int, floatなど)に変換するにあたって便利なのがSystem.BitConverterクラスです。しかしこのクラスは自分の環境におけるエンディアンで処理されてしまいます。おそらく多くの環境はリトルエンディアンですが、この場合ビッグエンディアンとしての処理はできないということになります。なんでないのかなーとは思いつつ、自作します。

ちなみに、世の中にはこれ以外のエンディアン(PDPとか)も存在しますが、今回は無視します。

エンディアンの列挙型

とりあえず前提としてこんなenumを作っておきます。

enum Endian
{
    Little, Big
}

プリミティブ型の変換

ビッグエンディアンとリトルエンディアンの相互変換は、バイト配列を逆順にすればいいだけなので難しくありません。

自分の環境のエンディアンは、BitConverter.IsLittleEndianで確かめることができます。これと指定されたエンディアンを見て、変換の必要があればバイト配列を逆順にします。

static byte[] Reverse(byte[] bytes, Endian endian)
{
    if(BitConverter.IsLittleEndian ^ endian == Endian.Little)
        return bytes.Reverse().ToArray();
    else 
        return bytes;
}

これを使って、たとえばintへ変換するメソッドはこう書けます。intの分の4バイトを配列から切り出してきて、エンディアンに従い必要があれば反転し、あとはBitConverterに任せます。他の型でもまったく同様に作れます。

static int ToInt32(byte[] value, int startIndex, Endian endian)
{
    byte[] sub = GetSubArray(value, startIndex, sizeof(int));
    return BitConverter.ToInt32(Reverse(sub, endian), 0);
}

// バイト配列から一部分を抜き出す
static byte[] GetSubArray(byte[] src, int startIndex, int count)
{
    byte[] dst = new byte[count];
    Array.Copy(src, startIndex, dst, 0, count);
    return dst;
}

任意の構造体(blittable)の変換

これはあまり合ってるか自信がありません。一応そこそこ動いているように思うので書いておきます。

リフレクションを使います。構造体の型情報からフィールド情報をprivateのものも含めすべて取得します。そこからフィールドの順番やフィールドのsizeofを取得し、それに従いバイト配列から読み取っていきます。フィールド情報を得るにはType.GetFieldsメソッドを使います。定義順になった配列が返ってきてくれるようで好都合です。エンディアンが変わっても、フィールドそれぞれの中のメモリ配置が変わるだけでフィールドの順番は変わらないので、はじめから順にバイト配列から読み取っていけば良いです。

FieldInfo[] fields = typeof(Hoge).GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);


これでバイト配列のどの部分をどのフィールドとして解釈するかは決まりました。しかしどのように解釈するかが課題になります。そのフィールドがintやdoubleなどプリミティブ型なら話は早いのですが、フィールドもまた構造体というケースも考えなければなりません。こういうやつです。

struct Hoge
{
    int A;
    Fuga B;
}
struct Fuga
{
    ....

ただしそのケースでも、どこまでも追っていけばいつかはプリミティブ型に行きつくはずです。クラスではHogeがFugaフィールドを持ち、FugaがHogeフィールドを持つという循環参照が可能ですが、構造体ではできません。ですから、フィールドがプリミティブ型のときはToInt32のようなメソッドを呼んで変換してやり、構造体のときはまた再帰で変換関数を呼んでやればよいということになります。

以上の考えのもと実装したのが以下のメソッドです。ToInt16やToSingleなどのプリミティブ型用メソッドはすべて適宜実装しているのが前提です。

参照型をフィールドに持つ構造体には変換できません。いわゆるblittableな構造体限定です。ですがバイト配列から参照型を持つ構造体に変換するという機会は、今のところ思いつきません。

static object ToStruct(byte[] value, int startIndex, Endian endian, Type type)
{
    if (!type.IsValueType)
        throw new ArgumentException();

    // プリミティブ型は専用メソッドへ飛ばす
    TypeCode code = Type.GetTypeCode(type);
    switch (code)
    {
        case TypeCode.Boolean:
            return ToBoolean(value, startIndex, endian);
        case TypeCode.Byte:
            return value[startIndex];
        case TypeCode.Char:
            return ToChar(value, startIndex, endian);
        case TypeCode.Double:
            return ToDouble(value, startIndex, endian);
        case TypeCode.Int16:
            return ToInt16(value, startIndex, endian);
        case TypeCode.Int32:
            return ToInt32(value, startIndex, endian);
        case TypeCode.Int64:
            return ToInt64(value, startIndex, endian);
        case TypeCode.SByte:
            return value[startIndex];
        case TypeCode.Single:
            return ToSingle(value, startIndex, endian);
        case TypeCode.UInt16:
            return ToUInt16(value, startIndex, endian);
        case TypeCode.UInt32:
            return ToUInt32(value, startIndex, endian);
        case TypeCode.UInt64:
            return ToUInt64(value, startIndex, endian);
        default:
            break; // 多分その他のstructなので以下処理する
    }

    // 構造体の全フィールドを取得
    FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
    // 型情報から新規インスタンスを生成 (返却値)
    object obj = Activator.CreateInstance(type);
    int offset = 0;
    foreach (FieldInfo info in fields)
    {
        // フィールドの値をバイト列から1つ取得し、objの同じフィールドに設定
        Type fieldType = info.FieldType;
        if (!fieldType.IsValueType)
            throw new InvalidOperationException();
        object fieldValue = ToStruct(value, startIndex + offset, endian, fieldType);
        info.SetValue(obj, fieldValue);
        // 次のフィールド値を見るためフィールドのバイトサイズ分進める
        offset += Marshal.SizeOf(fieldType);
    }

    return obj;
}

まとめ

今回の内容をまとめてクラスにしたものです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;

namespace Mpo2Jpg
{
    /// <summary>
    /// 指定したエンディアンでバイト列と基本データ型の変換ができるクラス
    /// </summary>
    static class BitConverterEx
    {
        /// <summary>
        /// バイト配列内の指定位置にある1バイトから変換されたBooleanを返す
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static bool ToBoolean(byte[] value, int startIndex, Endian endian)
        {
            byte[] sub = GetSubArray(value, startIndex, sizeof(bool));
            return BitConverter.ToBoolean(Reverse(sub, endian), 0);
        }
        /// <summary>
        /// バイト配列内の指定位置にある2バイトから変換されたUnicode文字を返す
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static char ToChar(byte[] value, int startIndex, Endian endian)
        {
            byte[] sub = GetSubArray(value, startIndex, sizeof(char));
            return BitConverter.ToChar(Reverse(sub, endian), 0);
        }
        /// <summary>
        /// バイト配列内の指定位置にある2バイトから変換された16ビット符号付き整数を返す
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static short ToInt16(byte[] value, int startIndex, Endian endian)
        {
            byte[] sub = GetSubArray(value, startIndex, sizeof(short));
            return BitConverter.ToInt16(Reverse(sub, endian), 0);
        }
        /// <summary>
        /// バイト配列内の指定位置にある8バイトから変換された16ビット符号無し整数を返す
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static ushort ToUInt16(byte[] value, int startIndex, Endian endian)
        {
            byte[] sub = GetSubArray(value, startIndex, sizeof(ushort));
            return BitConverter.ToUInt16(Reverse(sub, endian), 0);
        }
        /// <summary>
        /// バイト配列内の指定位置にある4バイトから変換された32ビット符号付き整数を返す
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static int ToInt32(byte[] value, int startIndex, Endian endian)
        {
            byte[] sub = GetSubArray(value, startIndex, sizeof(int));
            return BitConverter.ToInt32(Reverse(sub, endian), 0);
        }
        /// <summary>
        /// バイト配列内の指定位置にある4バイトから変換された32ビット符号無し整数を返す
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static uint ToUInt32(byte[] value, int startIndex, Endian endian)
        {
            byte[] sub = GetSubArray(value, startIndex, sizeof(uint));
            return BitConverter.ToUInt32(Reverse(sub, endian), 0);
        }
        /// <summary>
        /// バイト配列内の指定位置にある8バイトから変換された64ビット符号付き整数を返す
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static long ToInt64(byte[] value, int startIndex, Endian endian)
        {
            byte[] sub = GetSubArray(value, startIndex, sizeof(long));
            return BitConverter.ToInt64(Reverse(sub, endian), 0);
        }
        /// <summary>
        /// バイト配列内の指定位置にある8バイトから変換された64ビット符号無し整数を返す
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static ulong ToUInt64(byte[] value, int startIndex, Endian endian)
        {
            byte[] sub = GetSubArray(value, startIndex, sizeof(ulong));
            return BitConverter.ToUInt64(Reverse(sub, endian), 0);
        }
        /// <summary>
        /// バイト配列内の指定位置にある4バイトから変換された32ビット浮動小数点数を返す
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static float ToSingle(byte[] value, int startIndex, Endian endian)
        {
            byte[] sub = GetSubArray(value, startIndex, sizeof(float));
            return BitConverter.ToSingle(Reverse(sub, endian), 0);
        }
        /// <summary>
        /// バイト配列内の指定位置にある8バイトから変換された64ビット浮動小数点数を返す
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static double ToDouble(byte[] value, int startIndex, Endian endian)
        {
            byte[] sub = GetSubArray(value, startIndex, sizeof(double));
            return BitConverter.ToDouble(Reverse(sub, endian), 0);
        }

        /// <summary>
        /// バイト配列内の指定位置にあるバイト列から構造体に変換する
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static T ToStruct<T>(byte[] value, int startIndex, Endian endian)
            where T : struct
        {
            return (T)ToStruct(value, startIndex, endian, typeof(T));
        }
        /// <summary>
        /// バイト配列内の指定位置にあるバイト列から構造体に変換する
        /// </summary>
        /// <param name="value">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        public static object ToStruct(byte[] value, int startIndex, Endian endian, Type type)
        {
            if (!type.IsValueType)
                throw new ArgumentException();

            // プリミティブ型は専用メソッドへ飛ばす
            TypeCode code = Type.GetTypeCode(type);
            switch (code)
            {
                case TypeCode.Boolean:
                    return ToBoolean(value, startIndex, endian);
                case TypeCode.Byte:
                    return value[startIndex];
                case TypeCode.Char:
                    return ToChar(value, startIndex, endian);
                case TypeCode.Double:
                    return ToDouble(value, startIndex, endian);
                case TypeCode.Int16:
                    return ToInt16(value, startIndex, endian);
                case TypeCode.Int32:
                    return ToInt32(value, startIndex, endian);
                case TypeCode.Int64:
                    return ToInt64(value, startIndex, endian);
                case TypeCode.SByte:
                    return value[startIndex];
                case TypeCode.Single:
                    return ToSingle(value, startIndex, endian);
                case TypeCode.UInt16:
                    return ToUInt16(value, startIndex, endian);
                case TypeCode.UInt32:
                    return ToUInt32(value, startIndex, endian);
                case TypeCode.UInt64:
                    return ToUInt64(value, startIndex, endian);
                default:
                    break; // 多分その他のstructなので以下処理する
            }

            // 構造体の全フィールドを取得
            FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            // 型情報から新規インスタンスを生成 (返却値)
            object obj = Activator.CreateInstance(type);
            int offset = 0;
            foreach (FieldInfo info in fields)
            {
                // フィールドの値をバイト列から1つ取得し、objの同じフィールドに設定
                Type fieldType = info.FieldType;
                if (!fieldType.IsValueType)
                    throw new InvalidOperationException();
                object fieldValue = ToStruct(value, startIndex + offset, endian, fieldType);
                info.SetValue(obj, fieldValue);
                // 次のフィールド値を見るためフィールドのバイトサイズ分進める
                offset += Marshal.SizeOf(fieldType);
            }

            return obj;
        }

        /// <summary>
        /// バイト配列から一部分を抜き出す
        /// </summary>
        /// <param name="src">バイト配列</param>
        /// <param name="startIndex">value 内の開始位置</param>
        /// <param name="count">切り出すバイト数</param>
        /// <returns></returns>
        private static byte[] GetSubArray(byte[] src, int startIndex, int count)
        {
            byte[] dst = new byte[count];
            Array.Copy(src, startIndex, dst, 0, count);
            return dst;
        }
        /// <summary>
        /// エンディアンに従い適切なようにbyte[]を変換
        /// </summary>
        /// <param name="bytes">バイト配列</param>
        /// <param name="endian">エンディアン</param>
        /// <returns></returns>
        private static byte[] Reverse(byte[] bytes, Endian endian)
        {
            if(BitConverter.IsLittleEndian ^ endian == Endian.Little)
                return bytes.Reverse().ToArray();
            else 
                return bytes;
        }
    }
}

おわりに

最後にこんなことをした動機を書いておきます。先日の記事で触れたステレオ3Dカメラの画像フォーマットMPOにおいて、ニンテンドー3DSで撮った画像ではビッグエンディアンになるらしいという問題があるためです。MPOファイルのヘッダの最初の方にビッグなのか (0x49, 0x49, 0x2A, 0x00) リトルなのか (0x4D, 0x4D, 0x00, 0x2A) の情報がありますが、3DSではビッグのようです。おかげでちょっと面倒です。