С# interop: плохое взаимодействие между фиксированным и MarshalAs

Мне нужно объединить некоторые вложенные структуры в С# 4.0 в двоичные капли, чтобы перейти к платформе С++.

Я до сих пор имел большой успех, используя unsafe/fixed для обработки массивов фиксированной длины примитивных типов. Теперь мне нужно обработать структуру, содержащую вложенные массивы фиксированной длины других структур.

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

К сожалению, пока он дает мне правильное количество данных, он, похоже, также останавливает массивы fixed от правильной сортировки, как показывает результат этой программы. Вы можете подтвердить сбой, поставив точку останова на последней строке и исследуя память на каждом указателе.

using System;
using System.Threading;
using System.Runtime.InteropServices;

namespace MarshalNested
{
  public unsafe struct a_struct_test1
  {
    public fixed sbyte a_string[3];
    public fixed sbyte some_data[12];
  }

  public struct a_struct_test2
  {
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public sbyte[] a_string;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public a_nested[] some_data;
  }

  public unsafe struct a_struct_test3
  {
    public fixed sbyte a_string[3];
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public a_nested[] some_data;
  }


  public unsafe struct a_nested
  {
    public fixed sbyte a_notherstring[3];
  }

  class Program
  {
    static unsafe void Main(string[] args)
    {
      a_struct_test1 lStruct1 = new a_struct_test1();
      lStruct1.a_string[0] = (sbyte)'a';
      lStruct1.a_string[1] = (sbyte)'b';
      lStruct1.a_string[2] = (sbyte)'c';

      a_struct_test2 lStruct2 = new a_struct_test2();
      lStruct2.a_string = new sbyte[3];
      lStruct2.a_string[0] = (sbyte)'a';
      lStruct2.a_string[1] = (sbyte)'b';
      lStruct2.a_string[2] = (sbyte)'c';

      a_struct_test3 lStruct3 = new a_struct_test3();
      lStruct3.a_string[0] = (sbyte)'a';
      lStruct3.a_string[1] = (sbyte)'b';
      lStruct3.a_string[2] = (sbyte)'c';

      IntPtr lPtr1 = Marshal.AllocHGlobal(15);
      Marshal.StructureToPtr(lStruct1, lPtr1, false);

      IntPtr lPtr2 = Marshal.AllocHGlobal(15);
      Marshal.StructureToPtr(lStruct2, lPtr2, false);

      IntPtr lPtr3 = Marshal.AllocHGlobal(15);
      Marshal.StructureToPtr(lStruct3, lPtr3, false);

      string s1 = "";
      string s2 = "";
      string s3 = "";
      for (int x = 0; x < 3; x++)
      {
        s1 += (char) Marshal.ReadByte(lPtr1+x);
        s2 += (char) Marshal.ReadByte(lPtr2+x);
        s3 += (char) Marshal.ReadByte(lPtr3+x);
      }

      Console.WriteLine("Ptr1 (size " + Marshal.SizeOf(lStruct1) + ") says " + s1);
      Console.WriteLine("Ptr2 (size " + Marshal.SizeOf(lStruct2) + ") says " + s2);
      Console.WriteLine("Ptr3 (size " + Marshal.SizeOf(lStruct3) + ") says " + s3);

      Thread.Sleep(10000);
    }
  }
}

Вывод:

Ptr1 (size 15) says abc
Ptr2 (size 15) says abc
Ptr3 (size 15) says a

Поэтому по какой-то причине он только сортирует первый символ моих строк fixed ANSI. Есть ли способ обойти это, или я сделал что-то глупое, не связанное с сортировкой?

Ответы

Ответ 1

Это случай отсутствия диагностики. Кто-то должен был высказаться и сказать, что ваша декларация не поддерживается. Если это кто-то является либо компилятором С#, производящим ошибку компиляции, либо маршаллером поля CLR, создающим исключение во время выполнения.

Не похоже, что вы не можете получить диагностику. Вы обязательно получите его, когда начнете использовать структуру, как предполагалось:

    a_struct_test3 lStruct3 = new a_struct_test3();
    lStruct3.some_data = new a_nested[4];
    lStruct3.some_data[0] = new a_nested();
    lStruct3.some_data[0].a_notherstring[0] = (sbyte)'a';  // Eek!

Что вызывает CS1666: "Вы не можете использовать буферы фиксированного размера, содержащиеся в незафиксированных выражениях. Попробуйте использовать фиксированный оператор". Не то, чтобы "попробовать этот" совет был полезным:

    fixed (sbyte* p = &lStruct3.some_data[0].a_notherstring[0])  // Eek!
    {
        *p = (sbyte)'a';
    }

Точная ошибка CS1666. Следующее, что вы попробуете, это поместить атрибут в фиксированный буфер:

public unsafe struct a_struct_test3 {
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public fixed sbyte a_string[3];
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public a_nested[] some_data;
}
//...

    a_struct_test3 lStruct3 = new a_struct_test3();
    lStruct3.some_data = new a_nested[4];
    IntPtr lPtr3 = Marshal.AllocHGlobal(15);
    Marshal.StructureToPtr(lStruct3, lPtr3, false);  // Eek!

Сохраняет компилятор С#, но теперь CLR говорит вверх, и вы получаете исключение TypeLoadException во время выполнения: "Дополнительная информация: не может маршализовать поле" a_string "типа" MarshalNested.a_struct_test3 ": недопустимая комбинация управляемых/неуправляемых типов (этот тип значения должен быть сопряжен с Struct).

Итак, в двух словах вы должны были получить либо CS1666, либо TypeLoadException в своей первоначальной попытке. Этого не произошло, потому что компилятор С# не был вынужден смотреть на плохую часть, он генерирует только CS1666 в инструкции, которая обращается к массиву. И это не произошло во время выполнения, потому что полевой маршаллер в CLR не пытался маршалировать массив, потому что он равен нулю. Вы можете подать отчет об обратной связи с ошибкой на сайте connect.microsoft.com, но я был бы очень удивлен, если они не будут закрывать его "по дизайну".


В общем, неясная деталь очень важна для маршаллера поля в среде CLR, фрагмента кода, который преобразует значения структуры и объекты класса из их управляемого макета в их неуправляемый макет. Он плохо документирован, Microsoft не хочет прибивать детали конкретной реализации. В основном потому, что они слишком сильно зависят от целевой архитектуры.

Важно то, является ли значение или объект более гибким. Он горит, когда управляемый и неуправляемый макет идентичен. Это происходит только тогда, когда каждый член типа имеет одинаковый размер и выравнивание в обоих макетах. Обычно это происходит, когда поля имеют очень простой тип значения (например, byte или int) или структуру, которая сама является blittable. Не известно, когда это bool, слишком много конфликтующих неуправляемых типов bool. Поле типа массива никогда не сглаживается, управляемые массивы не выглядят похожими на массивы C, поскольку они имеют заголовок объекта и элемент длины.

Наличие желаемого значения или объекта очень желательно, он избегает создания маршаллера из-за необходимости создавать копию. Нативный код получает простой указатель на управляемую память, и все, что необходимо, - это привязать память. Очень быстро. Также очень опасно, если декларация не соответствует, то собственный код может легко окрашиваться вне строк и повреждать кучу GC или стек кадров. Очень распространенная причина для программы, которая использует pinvoke для случайной бомбы с ExecutionEngineException, чрезвычайно трудно диагностировать. Такая декларация действительно заслуживает небезопасного ключевого слова, но компилятор С# не настаивает на этом. И это не так, компиляторам не разрешено делать какие-либо предположения о макете управляемого объекта. Вы сохраняете это безопасно, используя Debug.Assert() для возвращаемого значения Marshal.SizeOf<T>, оно должно быть точным совпадением со значением sizeof(T) в программе C.

Как уже отмечалось, массивы являются препятствием для получения ослабляемого значения или объекта. Ключевое слово fixed предназначено в качестве обходного пути для этого. CLR рассматривает его как непрозрачный тип значения без каких-либо членов, просто капля байтов. Нет заголовка объекта и элемента Length, как можно ближе к массиву C. И используется в коде С#, как если бы вы использовали массив в программе на C, вы должны использовать указатель для обращения к элементам массива и три раза проверять, что вы не окрашиваете вне строк. Иногда вы должны использовать фиксированный массив, когда вы объявляете объединение (перекрывающиеся поля), и вы перекрываете массив со значением. Отравляя сборщик мусора, он больше не может понять, содержит ли поле корень объекта. Не обнаружен компилятором С#, но надежно отключает исключение TypeLoadException во время выполнения.


Короче говоря, используйте fixed только для типа blittable. Смешивание полей типа фиксированного размера с полями, которые должны быть маршалированы, не может работать. И не полезно, объект или значение копируются в любом случае, поэтому вы можете использовать тип дружественного массива.