C# - 構造体配列とbyte配列の変換


前置き飛ばして[タイトルの内容を見たい方はこちらから](https://qiita.com/kob58im/items/99b45df5bc2d75ccf58a#%E6%A7%8B%E9%80%A0%E4%BD%93%E9%85%8D%E5%88%97%E3%82%92%E6%89%B1%E3%81%86)

# 構造体のレイアウト

環境に依存しないようなレイアウト属性を指定しておかないと、構造体情報をバイナリデータに出力してほかのPC環境で読み込むときに失敗する恐れがある。


```cs
// using System.Runtime.InteropServices; が必要
[StructLayout(LayoutKind.Sequential, Pack = 数値)]
```

細かく指定する場合：

```cs
// using System.Runtime.InteropServices; が必要
[StructLayout(LayoutKind.Explicit)]
struct Xxx
{
    [FieldOffset(数値)]
    byte a;
    [FieldOffset(数値)]
    int b;
}
```

https://ufcpp.net/study/csharp/interop/memorylayout/

https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute?view=netframework-4.8

あと、packingを2以上にした場合や、2byte以上の型のデータを扱う場合、エンディアンも気を付ける必要があるが、指定はできなさそう。

## 構造体に配列を持たせる／UnmanagedTypeを明示する

配列を持たせる場合、配列メンバに[`ByValArray`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedtype?view=netframework-4.8)と要素数を指定する。
可変長配列を持つことはできない。（構造体の実サイズが確定する必要があるためだと思われる。ポインタ的な持ち方は可能のはず。）

```cs
struct Xxx
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=要素数)]
    int[] data;
}
```

https://learn.microsoft.com/en-us/dotnet/standard/native-interop/customize-struct-marshalling

# 変換

## byte配列から構造体への変換

```cs
// using System.Runtime.InteropServices; が必要
// Xxxは任意の構造体の型名
static Xxx BytesToStruct(byte[] bytes)
{
    var gch = GCHandle.Alloc(bytes, GCHandleType.Pinned);                            // step1
    var result = (Xxx)Marshal.PtrToStructure(gch.AddrOfPinnedObject(), typeof(Xxx)); // step2
    gch.Free();                                                                      // step3
    return result;
}
```

step1にて、step2で[アドレスを取得](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.gchandle.addrofpinnedobject?view=netframework-4.8)するためのハンドルを取得する。[`GCHandleType.Pinned`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.gchandletype?view=netframework-4.8)を指定することでピン留めしているイメージ。
なお、副作用？として、GC(ガベージコレクタ)から回収されないようにする効果があるため、step3にあるように[`free()`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.gchandle.free?view=netframework-4.8)でピン留めを外す必要がある。

> <b>Normal</b>
> This handle type represents an opaque handle, meaning you cannot resolve the address of the pinned object through the handle. You can use this type to track an object and prevent its collection by the garbage collector. This enumeration member is useful when an unmanaged client holds the only reference, which is undetectable from the garbage collector, to a managed object.
> <b>Pinned</b>
> This handle type is similar to Normal, but <font color="blue">allows the address of the pinned object to be taken</font>. This <font color="red">prevents the garbage collector from moving the object</font> and hence undermines the efficiency of the garbage collector. <font color="red">Use the Free() method to free the allocated handle</font> as soon as possible.

公式ドキュメントへのリンク
[`GCHandle.Alloc`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.gchandle.alloc?view=netframework-4.8#system-runtime-interopservices-gchandle-alloc(system-object-system-runtime-interopservices-gchandletype))
[`Marshal.PtrToStructure`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.ptrtostructure?view=netframework-4.8#system-runtime-interopservices-marshal-ptrtostructure(system-intptr-system-type))


## 構造体からbyte配列への変換

```cs
// using System.Runtime.InteropServices; が必要
// Xxxは任意の構造体の型名
static byte[] StructToBytes(Xxx obj)
{
    int size = Marshal.SizeOf(typeof(Xxx));
    IntPtr ptr = Marshal.AllocHGlobal(size);
    Marshal.StructureToPtr(obj, ptr, false);
 
    byte[] bytes = new byte[size];
    Marshal.Copy(ptr, bytes, 0, size);
    Marshal.FreeHGlobal(ptr);
    return bytes;
}
```

公式ドキュメントへのリンク
[`typeof`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/type-testing-and-cast#typeof-operator)
[`Marshal.SizeOf(Type)`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.sizeof?view=netframework-4.8#system-runtime-interopservices-marshal-sizeof(system-type))
[`Marshal.AllocHGlobal(int)`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.allochglobal?view=netframework-4.8#system-runtime-interopservices-marshal-allochglobal(system-int32))
[`Marshal.StructureToPtr`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.structuretoptr?view=netframework-4.8#system-runtime-interopservices-marshal-structuretoptr(system-object-system-intptr-system-boolean))
[`Marshal.Copy(IntPtr,byte[],int,int)`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.copy?view=netframework-4.8#system-runtime-interopservices-marshal-copy(system-intptr-system-byte()-system-int32-system-int32))
[`Marshal.FreeHGlobal`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.freehglobal?view=netframework-4.8#system-runtime-interopservices-marshal-freehglobal(system-intptr))

## 構造体配列を扱う

byte配列にオフセット指定して読み書きする方式で実装してみた。

```cs
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct SampleStruct
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=5)]
    public short[] a;
}

class Sample
{
    static void CopyStructToBytes(SampleStruct obj, byte[] dest, int offset)
    {
        int size = Marshal.SizeOf(typeof(SampleStruct));
        IntPtr ptr = Marshal.AllocHGlobal(size);
        Marshal.StructureToPtr(obj, ptr, false);
    
        Marshal.Copy(ptr, dest, offset, size);
        Marshal.FreeHGlobal(ptr);
    }

    static SampleStruct BytesToStruct(byte[] bytes, int offset)
    {
        var gch = GCHandle.Alloc(bytes, GCHandleType.Pinned);
        var result = (SampleStruct)Marshal.PtrToStructure(gch.AddrOfPinnedObject()+offset, typeof(SampleStruct));
        gch.Free();
        return result;
    }

    // 以下使用例

    static readonly int n=5;

    [STAThread]
    static void Main(string[] args)
    {
        // SampleStruct x = new SampleStruct();
        // x.a = new short[n]; // 構造体をnewで生成した時点ではaはnull扱いとなるよう。

        SampleStruct[] y = new SampleStruct[10];

        // 構造体のメンバ値を初期化
        for(int i=0;i<y.Length;i++)
        {
            y[i].a = new short[n];
            for(int k=0;k<n;k++)
            {
                y[i].a[k] = (short)(100*i+k);
            }
        }

        int sizePerElem = Marshal.SizeOf(typeof(SampleStruct));
        byte[] b = new byte[sizePerElem * y.Length];
        
        // バイト配列に変換
        for(int i=0;i<y.Length;i++) { CopyStructToBytes(y[i], b, i*sizePerElem); }

        // コンソールに出力
        for(int i=0;i<b.Length;i++) { Console.WriteLine("b[{0}] = {1}", i, b[i]); }

        // 構造体に変換（byte配列から復元）
        SampleStruct[] z = new SampleStruct[y.Length];
        for(int i=0;i<y.Length;i++) { z[i] = BytesToStruct(b, i*sizePerElem); }

        // コンソールに出力
        for(int i=0;i<y.Length;i++)
        {
            for(int k=0;k<n;k++)
            {
                Console.WriteLine("z[{0}].a[{1}] = {2}", i, k, z[i].a[k]);
            }
        }
    }
}
```

## その他の方法

`byte[]`とかは管理情報を持っているので、強引に構造体⇔配列の間でキャストするのは無理そう。
ちなみに、`Marshal.SizeOf`に配列をぶち込むと下記のようなエラーがでます。
```
ハンドルされていない例外: System.ArgumentException: 型 'SampleStruct[]' はアンマネージ構造体としてマーシャリングできません。有効なサイズ、またはオフセットの計算ができません。
   場所 System.Runtime.InteropServices.Marshal.SizeOfHelper(Type t, Boolean throwIfNotMarshalable)
```

`unsafe`を使って低レイヤーでbyte列をコピーすることはおそらく可能。（速度面でどうしても必要な場合は。）

https://www.sejuku.net/blog/103970


# 参考サイト

https://tomosoft.jp/design/?p=4647

↓以下は古い記事なので最適な手法かは不明。考え方は参考になりそう。

http://hongliang.seesaa.net/article/10208235.html

http://www2u.biglobe.ne.jp/~kaduhiko/csharp_05.html