2017-01-26 5 views
0

У меня есть общий метод для сериализации массива любого типа 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, не должно быть проблемой (должно быть?).

Хотя я бы приветствовал альтернативные или улучшенные методы сериализации, я гораздо больше заинтересован в прибивании этой конкретной ошибки. Благодаря!

Update Благодаря TnTnMn, указывая мне, что char не blittable типа, я посмотрел юникод символов ввода, чтобы увидеть, если они были сортировочной правильно. Оказывается, они НЕ.

Для CharBox { 0x2022, .15782328, .266239136, .164901689, .271627158 }, сериализации (в шестнадцатеричном) должно быть:

22 20 00 00 (Символ *)

6D 9C 21 3E (слева)

7F 50 88 3E (Top)

FD DB 28 3E (справа)

B7 12 8B 3E (Bottom)

(* Поскольку я не использовал явный макет, он дополнялся четырьмя байтами; Я сейчас разочарован собой за ненужное увеличение размера данных на 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.

Итак, это, по крайней мере, подтверждает, что происходит какое-то непреднамеренное поведение, и дает нам хорошую рабочую теорию относительно того, какие условия (а именно, символ Юникода в структуре) могут привести к ошибке.

То, что он не объясняет, - это то, почему это привело бы к возникновению исключения вообще, не говоря уже о том, почему он не поднят ни на одной машине, кроме этого пользователя. Что касается первого, то несоответствие в размере unciode против ANSI, я полагаю, должно соответствовать сообщению об ошибке («Область данных, переданная системному вызову, слишком мала»), но неуправляемый буфер - который размером 4 байта для char, будет больше, чем необходимо, а не меньше. Почему CLR или ОС расстраиваются при написании только 1 байт в область, предназначенную для 2 и достаточно большую для 4?

Что касается последнего, я подумал, что, возможно, пользователь может быть на более низкой версии .NET, чем все остальные, что может быть, если она не получит все обновления для Windows 7. Но я просто попробовал это на виртуальной машине с новой установкой Windows 7 и .NET 4.5 (самая низкая версия, которую поддерживает приложение) и по-прежнему не может воспроизвести ошибку. Я пытаюсь выяснить, какая именно версия .NET у нее есть, если она равна 4.5.1 или что-то в этом роде. Тем не менее, это кажется длинным выстрелом.

Кажется, что единственный способ, чтобы знать наверняка будет изменить Character члена int (чтобы отступы то же самое для существующих данных) и только привести его к char, когда это необходимо, а затем посмотреть, что изменяет результат на машине пользователя. Это также будет хорошей возможностью обернуть каждый отдельный вызов Marshal в обработчике исключений, поскольку Джон предложил посмотреть, что именно вызывает ошибку.

Хорошая новость заключается в том, что это довольно низкоприоритетная функция, поэтому я могу позволить ей выйти из строя безопасно, даже если она будет продолжаться.

Отчитается. Спасибо всем.

+0

Является ли 'CharBox' в той же DLL или EXE, где вызывается функция SerializeArray()? Если нет, то «внутренняя» - это твоя проблема, я бы подумал. – John

+0

Это в том же модуле. –

+0

ОК, следующим шагом будет определить, какой из системных вызовов выбрасывает это исключение. Когда мы используем маршалл, мы религиозно используем утверждения try..catch вокруг каждого вызова. Тогда вы будете знать, где искать дальше. Могут быть всевозможные причины этой общей ошибки. – John

ответ

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; 
    } 

В зависимости от вас использования, возможно, потребуется, чтобы обеспечить механизм для уникального имени (где я использовал «Фред») для отображения файла в памяти.

+0

Хорошо, что у него есть преимущество для оригинальности! Я попробую, спасибо. Все еще надеялся пригвоздить то, что я делаю неправильно здесь (или если в рамках есть ошибка). –

+0

Это сегмент разделяемой памяти! Overkill ...... и любопытно/сомневаюсь в производительности. – John

+0

@john, я провел несколько тестов на Win 10/64. Скомпилированный как 32-битная сборка, технология MMF занимала примерно в два раза больше, чем версия OP в 3,32 мс против 1,75 мс для обработки 10000 элементов. При компиляции в виде 64-битной сборки метод MMF очень слабо выполнял исходную версию 1.48 мс против 1,56 мс. Теперь, если Char fikled изменен на свойство, которое сопоставляется с blittable integer, метод OP занимает примерно половину времени, который он делал ранее, и время MMF оставалось неизменным. – TnTinMn

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 «s в struct

Мое предположение, что установка раскладки Explicit и CharSet в Unicode будет иметь было достаточно, чтобы позволить Character снова быть char, но я бы предпочел не тратить время моего клиента на большее количество проб и ошибок, так как он работает. Надеюсь, кто-то еще может рассказать о том, что произошло, но я, вероятно, отправлю это в MSDN тоже в надежде, что один из богов CLR может иметь некоторое представление.

Спасибо всем, особенно TnTnMan, потому что подсветка вопроса с char и с дробящими определенно мотивировала меня на эти изменения.

+0

Для дальнейшего использования, теперь, когда ваша структура содержит только светлые типы, вы можете использовать 'gchnd = GCHandle.Alloc (массив, GCHandleType.Pinned)' для получения экземпляра GCHandle. Затем вы можете сделать один «Marshal.Copy (gchnd.AddrOfPinnedObject, rawData, 0, rawData.Length)» для загрузки массива байтов. Нет необходимости копировать каждый элемент. Вы можете использовать тот же метод для воссоздания массива из массива байтов. – TnTinMn

+0

О, это отличная идея, спасибо. Если вы хотите, не стесняйтесь публиковать это как ответ, потому что это, вероятно, лучший подход для всех. –