【C#】クラス配列にMarshalAsを設定するとPtrToStructureができない

やりたかったこと
バイナリファイルから読み込んだデータを、オブジェクトにMemcpyする。クラスのメンバ変数にちまちまアクセスしてコピーするのでなく、サイズ指定で一括でコピーしたい。
開発環境
  • Windows10
  • Microsoft Visual Studio Community2019
  • .NET Framework 4.7.2

Memcpyができなかった

読み込みたいバイナリデータ

読み込むためのクラス

    class UnmanageRead
    {
        byte[] str = new byte[16];

        UInt32 flg;

        Inner[] inner = new Inner[4];

        class Inner
        {
            byte inner1;
            byte inner2;
        }
    }

 

C#でMemCpyするためには、UnmanageReadクラスにUnManagement属性をつける必要があります。

属性をつけない場合は、領域のサイズが保証されません。byte型で定義したとしてもコンパイラがint型で定義しなおします。UnManagement属性で領域のサイズを保証させます。

それで、さきほどのクラスにUnManagement属性をつけてみました。

UnManagement属性をつけたクラス

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    class UnmanageRead
    {
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
        byte[] str;

        UInt32 flg;

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        Inner[] inner;

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        class Inner
        {
            byte inner1;
            byte inner2;
        }
    }

バイナリファイルから読み込んだデータをMemcpyしてみます。

コードは以下のようになります。

    class Program
    {
        static void Main(string[] args)
        {
            // バイナリファイルを読み込むためのストリームの作成
            FileStream stream = new FileStream(
                      System.AppDomain.CurrentDomain.BaseDirectory
                              + "test.bin", FileMode.Open);

            // 読み込み先のオブジェクトを作成
            UnmanageRead obj = new UnmanageRead();

            // MemCpyでオブジェクトに保存する
            // 0x28 はバイナリファイルのデータサイズ
            int size = 0x28;
            byte[] bytes = new byte[size];
            stream.Read(bytes, 0, size);

            IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(UnmanageRead)));
            Marshal.StructureToPtr(obj, ptr, false);
            Marshal.Copy(bytes, 0, ptr, size);
            obj = (UnmanageRead)Marshal.PtrToStructure(ptr, typeof(UnmanageRead));
            Marshal.FreeHGlobal(ptr);

            stream.Close();
        }
    }

しかし、このコードだと、22行目のPtrToStructure の処理で例外ExecutionEngineExceptionが発生しました

例外が発生した原因

原因を調査するうちに、公式サイトで以下の記述を見つけました。

Value プロパティを ByValArray に設定した場合、SizeConst フィールドは、配列の要素数を示すように設定する必要があります。 ArraySubType フィールドには、文字列型を区別する必要がある場合に、オプションとして配列要素の UnmanagedType を格納できます。 この UnmanagedType は、要素が構造体にフィールドとして定義されている配列でのみ使用できます。

引用元 : UnmanagedType 列挙型

先ほどのクラスで、

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        Inner[] inner;

と記述していました。Inner[] でクラス配列を定義しているためダメみたいです構造体配列は大丈夫のようです

なんで構造体配列だけ大丈夫なの?クラス配列も許可してよー。

と思いました。

でも、よくよく考えたら構造体配列とクラス配列で大きな違いがありました。前者は値の配列、後者は参照の配列だったのです

だから、ここではクラスの参照先の配列を定義していたことになるのです。Innerクラスの実体はメモリ上のどこにあるかわかりません。

なので、PtrToStructure の関数で参照先に変な値を書き込もうとして例外が発生したと思われます。

対策

Innerクラス→Inner構造体に変更するだけで解決しました。

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    class UnmanageRead
    {
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
        byte[] str;

        UInt32 flg;

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        Inner[] inner;

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        // ★★★ classからstruct に変更
        struct Inner
        {
            UInt16 inner1;
            UInt16 inner2;
            UInt16 inner3;
            UInt16 inner4;
            UInt16 inner5;
            UInt16 inner6;
        }
    }

Inner構造体となり、UnmanageRead のオブジェクトは、Inner 構造体2個分の領域を確保します。

構造体が嫌ならば、Memcpyを諦めるしかないですね。。。

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です