2013/11/26

C#Tips7 バイト列と構造体のオフセット

まずはC#でデータをためていく方法について考えましょう。

素朴にstringに加えていきます。
class Program {
    public static void Main() {
        string str = "";

        str += "abc";
        str += "def";

        System.Console.WriteLine(str);
    }
}

C/C++から考えると恐ろしいですが、実際には+=は末尾に付け加えるメソッドになっています。

ところが文字列でないデータ列の場合、これでは読み出せません。なぜならば、
str[n]としたところでn文字目が読み出されるからです。やはり8bitごと区切られたデータが欲しい。
それではListを使いましょうか?

using System.Collections.Generic;

class Program {
    public static void Main() {
        List<byte> data = new List<byte>();

        data.Add(0x10);
        data.Add(0x20);
        data.Add(0x30);
        data.Add(0x40);

        foreach(byte value in data) {
            System.Console.WriteLine(value);
        }
    }
}

うむ……これでは配列が欲しいとき困る。
そんなときにMemoryStreamです。可変長で、好きなときにWriteで書き込みできます。(内部の実装はお察し)
データをためて行くにはWriteしていけばよいです。

using System.IO;

class Program {
    public static void Main() {
        MemoryStream ms = new MemoryStream();

        ms.Write(new byte[] { 0x10 }, 0, 1);
        ms.Write(new byte[] { 0x20 }, 0, 1);
        ms.Write(new byte[] { 0x30, 0x40 }, 0, 2);

        byte[] data = ms.ToArray();

        foreach(byte value in data) {
            System.Console.WriteLine(value);
        }
    }
}

さて本題。今、0byte目, 1byte目, 2byte目に意味のある数字が並んでおり、それぞれa, b, cと名付けましょう。
そして、0, 2byte目は8bitの非負整数、1byte目は16bitの非負整数であるとします。与えられたストリーム(byte列としましょう)からこれらに振り分けるにはどうしたらいいでしょうか?

力尽くで、if文など駆使して書けるでしょうが、d, e, f, ...と続く場合は見づらいコードになります。
ここはC/C++のような構造体を使ってシンプルに書きたいものです。が、C#にはわかりやすい書き方はありません。
正確に言うと余り推奨していないやり方なのでしょう。
以下の例は構造体にオフセットを用いて各データを並べています。
これはごく普通のやり方なのですが、System.Runtime.InteropServices.StructLayout()で属性を設定します。
LayoutKind.Explicitを指定することでFieldOffsetにより構造体の位置を指定します。

先頭が0byte目で、例えば[FieldOffset(0)]の次に来るpublic なんちゃらは、0byte目のメモリ空間を使うことになります。[FieldOffset(1)]ならば次に来るpublicなんちゃらは1byte目のメモリ空間を使うことになります。
おなじ[FieldOffset(x)]を設定したら、それらはメモリの開始位置を共有します。

byteなら1byte, shortなら2byte, intなら4byteのように領域が決まっていますが、
例えばbyte, short, intの3つのフィールドを[FieldOffset(0)]で宣言したとき、
Bxxx
SSxx
IIII
のように、共有します。即ち、byteのデータはshortの最下位バイトと共有し、intの最下位バイトとも共有するわけです。先頭なのに何で最下位か? これはリトルエンディアンと呼ばれる方式で、
0x11223344という32bitの整数は「0x44」「0x33」「0x22」「0x11」の順番でメモリに格納されます。
従って共有されるのは最下位バイトなのです。

さて、実際に宣言してみると問題が発生します。
というのも、C#のbyte配列というものはオブジェクトであって、メモリをリニアーに配置した値型ではないのです。簡単に言うとポインタであってメモリ領域ではないのです。C/C++では構造体は配列と同じくデータ領域でしかなかったため柔軟に対応できました。
この問題を解決するためにC#では固定長配列が宣言できます。ただしunsafeで。ぶっちゃけunsafeなんか使うくらいならC/C++使った方がいいと思うんですけど(外部ライブラリ作ってでもいい)、やってみました。
fixed 基本型名 変数名[データ数]
で固定長が宣言できます。

using System.Runtime.InteropServices;

class Program {
    [StructLayout(LayoutKind.Explicit)]
    unsafe struct S {
        [FieldOffset(0)]
        public fixed byte data[4];

        [FieldOffset(0)]
        public byte a;
        [FieldOffset(1)]
        public ushort b;
        [FieldOffset(3)]
        public byte c;
    }
   
    public static void Main() {
        S s = new S();
        unsafe {
            byte[] data = new byte[] { 1, 2, 3, 4 };
            int i = 0;
            foreach(byte d in data) {
                s.data[i] = data[i];
                i++;
            }
        }
        System.Console.WriteLine("{0:X}, {1:X}, {2:X}", s.a, s.b, s.c);
    }
}

もっといい方法はないかね。

0 件のコメント:

コメントを投稿