将结构数组序列化为byte [] - 我的代码有什么问题?

时间:2017-01-26 00:07:50

标签: c# .net marshalling unmanaged-memory

我有一个通用方法,可以使用structbyte将任意Marshal.StructureToPtr类型的数组序列化为Marshal.Copy数组。完整的代码是:

    internal static byte[] SerializeArray<T>(T[] array) where T : struct
    {
        if (array == null)
            return null;
        if (array.Length == 0)
            return null;

        int position = 0;
        int structSize = Marshal.SizeOf(typeof(T));

        byte[] rawData = new byte[structSize * array.Length];

        IntPtr buffer = Marshal.AllocHGlobal(structSize);
        foreach (T item in array)
        {
            Marshal.StructureToPtr(item, buffer, false);
            Marshal.Copy(buffer, rawData, position, structSize );
            position += structSize;
        }
        Marshal.FreeHGlobal(buffer);

        return rawData;
    }

99.99%的时间完美无瑕。但是,对于我的一个Windows 7用户,使用某些输入数据时,此代码可能会导致以下非.NET异常:

  

传递给系统调用的数据区域太小。 (例外   HRESULT:0x8007007A)。

不幸的是,我无法访问用户的计算机以附加调试器,即使处理与用户完全相同的输入数据,我也无法复制该问题。这只发生在一个用户的机器上,只发生在某些输入数据上,但在她的机器上每次都会发生相同的输入数据,因此它绝对不是随机的。

该应用程序面向.NET 4.5。

有人能看到这段代码有什么问题吗?我唯一的猜测是,Marshal.SizeOf报告的内容与数据结构的实际大小之间存在一些不匹配,从而导致为结构分配的内存不足。

如果重要,这里是错误发生时序列化的结构(它是OCR产生的字符位置的表示):

public struct CharBox
{
    internal char Character;
    internal float Left;
    internal float Top;
    internal float Right;
    internal float Bottom;
}

正如您所看到的,所有字段应始终保持不变,因此我对每个struct序列化的非托管内存的单个固定长度段的初始分配应该不是问题(应该吗?)。

虽然我欢迎使用替代或改进的序列化方法,但我更感兴趣的是确定这个特定的bug。谢谢!

更新 感谢TnTnMn指出我char不是一个blittable类型,我在输入中查找unicode字符,看看它们是否正确编组。事实证明,他们不是。

对于CharBox { 0x2022, .15782328, .266239136, .164901689, .271627158 },,序列化(十六进制)应为:

  

22 20 00 00(Character *)

     

6D 9C 21 3E(左)

     

7F 50 88 3E(上)

     

FD DB 28 3E(右)

     

B7 12 8B 3E(底部)

(*由于我没有使用显式布局,它填充到四个字节;我现在因为不必要地将数据大小增加11%而感到沮丧...)

相反,它正在序列化为:

  

95 00 00 00(字符)

     

6D 9C 21 3E(左)

     

7F 50 88 3E(上)

     

FD DB 28 3E(右)

     

B7 12 8B 3E(底部)

因此它将char 0x2022编组为0x95。碰巧,0x2022 Unicode和0x95 ANSI都是子弹字符。因此,这不是随机的,而是将所有内容编组为ANSI,如果您没有指定CharSet,我现在还记得这是标准过程。

好的,所以这至少证实了一些非预期的行为正在进行,并且进一步为我们提供了一个关于什么条件(即结构中的unicode字符)可能导致错误的良好工作理论。

它没有解释的是为什么这会引发异常,更不用说为什么它不是在任何机器上引发的,而是这个用户的。对于前者,我认为unciode与ANSI的byte大小的差异与错误消息(“传递给系统调用的数据区域太小”)一致,但是不受管理缓冲区 - 其大小可容纳char的4个完整字节,将大于必要值,而不是更小。为什么CLR或操作系统不应该只将1个字节写入2个区域并且大到4个区域?

对于后者,我认为用户可能使用的.NET版本低于其他所有人,如果她没有获得所有Windows 7更新,则可能就是这种情况。但我刚刚在具有全新Windows 7安装和.NET 4.5(应用程序支持的最低版本)的VM上试用了它,但仍然无法重现错误。我正试图找出她所拥有的.NET版本,以防它是4.5.1或者什么的。不过,这似乎是一个长镜头。

似乎唯一可以确定的方法是将Character成员更改为int(以使现有数据的填充保持相同)并仅将其转换为{{1}必要时,然后查看是否更改了用户计算机上的结果。这也是一个很好的机会,可以在异常处理程序中包装每个不同的char调用,因为John建议查看哪个正是导致错误。

好消息是这是一个非常低优先级的功能,所以即使它继续发生,我也可以安全地失败。

将报告回来。谢谢大家。

2 个答案:

答案 0 :(得分:0)

我在你现有的方法中没有看到任何明显的错误,所以我没有在这方面提供任何东西。但是,既然你说:

  

我欢迎使用替代或改进的序列化方法

我想把它扔出去供你考虑。使用MemoryMappedViewAccessor执行从结构数组到字节数组的转换。这当然需要创建MemoryMappedFile

internal static byte[] SerializeArray<T>(T[] array) where T : struct
    {
    int unmananagedSize = Marshal.SizeOf(typeof(T));

    int numBytes = array.Length * unmananagedSize;
    byte[] bytes = new byte[numBytes];

    using (MemoryMappedFile mmf = MemoryMappedFile.CreateNew("fred", bytes.Length))
        {
        using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor(0, bytes.Length, MemoryMappedFileAccess.ReadWrite))
            {

            accessor.WriteArray<T>(0, array, 0, array.Length);
            accessor.ReadArray<byte>(0, bytes, 0, bytes.Length);

            }
        }

    return bytes;
    }

internal static T[] DeSerializeArray<T>(byte[] bytes) where T : struct
    {
    int unmananagedSize = Marshal.SizeOf(typeof(T));

    int numItems = bytes.Length / unmananagedSize;
    T[] newArray = new T[numItems];

    using (MemoryMappedFile mmf = MemoryMappedFile.CreateNew("fred", bytes.Length))
        {
        using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor(0, bytes.Length, MemoryMappedFileAccess.ReadWrite))
            {

            accessor.WriteArray<byte>(0, bytes, 0, bytes.Length);
            accessor.ReadArray<T>(0, newArray, 0, newArray.Length);

            }
        }
    return newArray;
    }

根据您的使用情况,您可能需要为MemoryMappedFile提供唯一名称(我使用“fred”)的机制。

答案 1 :(得分:0)

我找到了一个有效的解决方案,但我仍然不知道为什么。

这是我改变的。 CharBox现在是:

[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
public struct CharBox
{
    [FieldOffset(0)]
    internal int Character;

    [FieldOffset(4)]
    internal float Left;

    [FieldOffset(8)]
    internal float Top;

    [FieldOffset(12)]
    internal float Right;

    [FieldOffset(16)]
    internal float Bottom;

    // Assists with error reporting
    public override string ToString()
    {
        return $"CharBox (Character = {this.Character}, Left = {this.Left}, Top = {this.Top}, Right = {this.Right}, Bottom = {this.Bottom})";
    }
}

现在的实际方法是:

    internal static byte[] SerializeArray<T>(T[] array) where T : struct
    {
        if ( array.IsNullOrEmpty() )
            return null;            

        int position = 0;
        int structSize = Marshal.SizeOf(typeof(T));

        if (structSize < 1)
        {
            throw new Exception($"SerializeArray: invalid structSize ({structSize})");
        }

        byte[] rawData = new byte[structSize * array.Length];
        IntPtr buffer = IntPtr.Zero;

        try
        {
            buffer = Marshal.AllocHGlobal(structSize);
        }
        catch (Exception ex)
        {
            throw new Exception($"SerializeArray: Marshal.AllocHGlobal(structSize={structSize}) failed. Message: {ex.Message}");
        }

        try
        {
            int i = 0;
            int total = array.Length;
            foreach (T item in array)
            {
                try
                {
                    Marshal.StructureToPtr(item, buffer, false);
                }
                catch (Exception ex)
                {
                    throw new Exception($"SerializeArray: Marshal.StructureToPtr failed. item={item.ToString()}, index={i}/{total}. Message: {ex.Message}");
                }

                try
                {
                    Marshal.Copy(buffer, rawData, position, structSize);
                }
                catch (Exception ex)
                {
                    throw new Exception($"SerializeArray: Marshal.Copy failed. item={item.ToString()}, index={i}/{total}. Message: {ex.Message}");
                }

                i++;
                position += structSize;
            }
        }
        catch
        {
            throw;
        }
        finally
        {
            try
            {
                Marshal.FreeHGlobal(buffer);
            }
            catch (Exception ex)
            {
                throw new Exception($"Marshal.FreeHGlobal failed (buffer={buffer}. Message: {ex.Message}");
            }
        }

        return rawData;
    }

我希望能够获得有关错误的更多详细信息,但是用户报告说它没有任何警告就能正常工作。

SerializeArray的所有更改仅用于更详细的报告,因此实质性更改(其中一个或多个是获胜者)是:

  • char更改为int(我本来会使用short,但我希望与现有数据保持兼容,因为此struct用于其他地方,以前它使用的是4字节填充。

  • struct布局设置为LayoutKind.Explicit并设置显式FieldOffset s;以及

  • CharSet.Unicode中指定StructLayout - 由于char

    struct,因此可能无效>

我的猜测是将布局设置为Explicit而将CharSet设置为Unicode就足以让Character再次成为char,但我会而不是浪费我的客户的时间与更多的试验和错误,因为它工作。希望其他人可以对发生的事情发表意见,但我也可能将此发布到MSDN,希望其中一个CLR神可能有一些见解。

特别感谢TnTnMan,因为突出char的问题和blitting肯定会激励我尝试这些改变。