我有一个通用方法,可以使用struct
和byte
将任意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建议查看哪个正是导致错误。
好消息是这是一个非常低优先级的功能,所以即使它继续发生,我也可以安全地失败。
将报告回来。谢谢大家。
答案 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
我的猜测是将布局设置为Explicit
而将CharSet
设置为Unicode就足以让Character
再次成为char
,但我会而不是浪费我的客户的时间与更多的试验和错误,因为它工作。希望其他人可以对发生的事情发表意见,但我也可能将此发布到MSDN,希望其中一个CLR神可能有一些见解。
特别感谢TnTnMan,因为突出char
的问题和blitting肯定会激励我尝试这些改变。