3

我有一个通用方法,可以使用and将任何struct类型的数组序列化为 s 数组。完整的代码是:byteMarshal.StructureToPtrMarshal.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应该不是问题(应该吗?)。

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

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

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

22 20 00 00(字符*)

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 (底部)

所以它将char0x2022 编组为 0x95。碰巧的是,0x2022 Unicode 和 0x95 ANSI 都是项目符号字符。因此,这不是随机的,而是将所有内容编组到 ANSI,我现在记得,如果您不指定CharSet.

好的,所以这至少证实了一些意外的行为正在发生,并进一步为我们提供了一个很好的工作理论,即哪些条件(即结构中的 unicode 字符)可能导致错误。

它没有解释的是为什么这会引发异常,更不用说为什么除了这个用户之外的任何机器上都没有引发异常。至于前者,byte我认为 unciode 与 ANSI 的大小差异与错误消息(“传递给系统调用的数据区域太小”)一致,但非托管缓冲区 - 大小为 , 容纳 4 个完整字节char会比必要的大,而不是更小。为什么 CLR 或操作系统会因为只将 1 个字节写入打算用于 2 且足够大用于 4 的区域而感到不安?

至于后者,我认为用户可能使用的 .NET 版本可能比其他所有人都低,如果她没有获得所有 Windows 7 更新,可能就是这种情况。但我只是在安装了全新 Windows 7 和 .NET 4.5(应用程序支持的最低版本)的 VM 上进行了尝试,但仍然无法重现该错误。我正试图找出她所拥有的 .NET 版本,以防万一是 4.5.1 或其他版本。不过,这似乎是一个远大的目标。

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

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

会回来汇报的。谢谢大家。

4

2 回答 2

1

好吧,我找到了一个可行的解决方案,尽管我仍然不知道为什么。

这是我改变的。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都只是为了更详细的报告,因此实质性更改(其中一项或多项是获胜者)是:

  • 将 更改charint(我本来会使用short,但我想与现有数据保持兼容,因为它struct在其他地方使用,并且以前使用 4 字节填充)。

  • struct布局设置为LayoutKind.Explicit并设置显式FieldOffsets;和

  • 指定CharSet.Unicodein StructLayout- 诚然,这可能什么也没做,因为charstruct

我的猜测是,将布局设置ExplicitCharSetUnicode 就足以允许Character再次使用char,但我不想浪费客户的时间进行更多的试验和错误,因为它正在工作。希望其他人可以就发生的事情发表意见,但我也可能会将其发布到 MSDN,希望 CLR 大神之一可能有一些见解。

感谢所有人,尤其是 TnTnMan,因为用chars 和 blitting 突出问题肯定会激励我尝试这些更改。

于 2017-01-26T21:54:47.670 回答
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”)的机制。

于 2017-01-26T03:00:25.560 回答