Преобразование массива указателей в массив IntPtr

Я застрял в кажущейся тривиальной задаче и нуждаюсь в вашей помощи.

Мне нужно написать метод со следующей сигнатурой:

System.Array ToIntPtrArray(System.Array a)

где фактический аргумент может быть массивом любого типа указателя (например, int*[], long**[], void*[,]) и возврата массив такой же формы с элементами типа System.IntPtr с теми же численными значениями, что и элементы входного массива. Проблема в том, что я не понимаю, как извлечь числовые значения указателей, если я не знаю их типов заранее.


Например, если я заранее знал, что мой аргумент всегда имеет тип void*[], я мог бы написать метод следующим образом:

unsafe IntPtr[] ToIntPtrArray(void*[] a)
{
    var result = new IntPtr[a.Length];
    for (int i = 0; i < a.Length; i++)
        result[i] = (IntPtr) a[i];

    return result;
}

Но проблема в том, что это может быть не void*[], а void**[] или что-то еще, и метод должен иметь возможность обрабатывать все случаи.

Ответ 1

Короткий ответ: это невозможно сделать напрямую. Причиной является то, что если вы передадите свою функцию преобразования, любой из обычных контейнеров, поддерживающих индекс (System.Array, Collections.IList, ArrayList и т.д.), Выполняющие операции индекса будут пытаться передать результат в System.Object. Указатели в С# не выводятся из Object, поэтому это приведет к SystemNotSupported или аналогичному исключению.

Существует два разумных решения:

  • Преобразуйте массивы указателей, чтобы освободить указательные массивы до вызова Метод.
  • Преобразуйте указательные массивы в указатели указателей указателей перед вызов метода.

Первый довольно громоздкий, поскольку он требует дублирования всего содержимого массива с циклом for. Второй вариант требует передачи в длину массива, поскольку он больше не обернут управляемым объектом System.Array.

Пример кода

Метод:

    unsafe Array ToIntPtrArray(void** a, int count)
    {
        IntPtr[] intPtrArray = new IntPtr[count];

        for (int n = 0; n < count; n++)
            intPtrArray[n] = new IntPtr(a[n]);

        return intPtrArray;
    }

Пример использования (целочисленный указательный массив):

int*[] intPtrArray;

// Code that initializes the values of intPtrArray

fixed(int** ptr = &intPtrArray[0])
{
   Array result = ToIntPtrArray((void**)ptr, intPtrArray.Length);
}

Пример использования (указательный указатель указателя void):

void**[] voidPtrPtrArray;

// Code that initializes the values of voidPtrPtrArray

fixed(void*** ptr = &voidPtrPtrArray[0])
{
    Array result = ToIntPtrArray((void**)ptr, voidPtrPtrArray.Length);
}

Пример использования (многомерный массив указателей int):

int*[,] int2dArray;

// Code that initializes the values of int2dArray

fixed(int** ptr = &int2dArray[0,0])
{
    Array result = ToIntPtrArray((void**)ptr, TotalSize(int2dArray));
    Array reshaped = ReshapeArray(result,int2dArray);
}

Где TotalSize и ReshapeArray - вспомогательные функции, которые записываются для работы с многомерными массивами. Для получения советов о том, как это сделать, см. Программно объявить массив произвольного ранга.

Ответ 2

Это довольно сложная проблема. Создание массива правильной формы не так уж плохо.

unsafe System.Array ToIntPtrArray(System.Array a)
{
    int[] lengths = new int[a.Rank];
    int[] lowerBounds = new int[a.Rank];
    for (int i = 0; i < a.Rank; ++i)
    {
        lengths[i] = a.GetLength(i);
        lowerBounds[i] = a.GetLowerBound(i);
    }
    Array newArray = Array.CreateInstance(typeof (IntPtr), lengths, lowerBounds);

    // The hard part is iterating over the array.
    // Multiplying the lengths will give you the total number of items.
    // Then we go from 0 to n-1, and create the indexes
    // This loop could be combined with the loop above.
    int numItems = 1;
    for (int i = 0; i < a.Rank; ++i)
    {
        numItems *= lengths[i];
    }

    int[] indexes = new int[a.Rank];
    for (int i = 0; i < numItems; ++i)
    {
        int work = i;
        int inc = 1;
        for (int r = a.Rank-1; r >= 0; --r)
        {
            int ix = work%lengths[r];
            indexes[r] = lowerBounds[r] + ix;
            work -= (ix*inc);
            inc *= lengths[r];
        }

        object obj = a.GetValue(indexes);
        // somehow create an IntPtr from a boxed pointer
        var myPtr = new IntPtr((long) obj);
        newArray.SetValue(myPtr, indexes);
    }
    return newArray;
}

Это создает массив правильного типа и формы (размеры и длину), но у него есть проблема. Метод GetValue, который вы используете для получения элемента из массива, возвращает object. И вы не можете использовать тип указателя для object. Ни в коем случае, ни как. Таким образом, вы не можете получить значение из массива! Если вы вызываете GetValue в массиве long*, например, вы получите "тип не поддерживается".

Мне кажется, вам нужно каким-то образом скопировать этот нечетно-образный массив в одномерный массив int* (или любой другой тип указателя). Затем вы можете напрямую проиндексировать временный массив и получить значения, чтобы заполнить ваш массив IntPtr.

Это интересная проблема с курицей и яйцом. Если вы передадите его как System.Array, вы не сможете получить элементы из него, потому что нет пути преобразования от object до int* (или любого другого типа указателя). Но если вы передадите его как тип указателя (т.е. int**), то вы не сможете получить форму вещи.

Я полагаю, вы могли бы написать его как:

unsafe System.Array ToIntPtrArray(System.Array a, void** aAsPtr)

Затем у вас есть метаданные System.Array и фактические данные в форме, которую вы можете использовать.

Ответ 3

Несмотря на то, что на вопрос ответили хорошо, я чувствую необходимость оставить записку/предупреждение будущим читателям.

CLR предназначен для обеспечения безопасности или, по крайней мере, максимально безопасного. Он выполняет это с помощью (помимо всего прочего) безопасности и абстрагирования памяти. Хотя вы можете отключить некоторые из этих защит с помощью небезопасного блока, некоторые защиты жестко закодированы в компилятор/время выполнения. Чтобы обойти это дополнительное препятствие, нужно прибегнуть к хакке и, возможно, медленному коду. Несмотря на то, что эти методы работают, опыт научил меня, что такое поведение приводит к проблемам в будущем, будь то изменение среды выполнения, будущему программисту, нуждающемуся в изменении этого сегмента кода, или вам самому нужно что-то делать с этими указатели позже в коде.

В этот момент я бы серьезно рассмотрел вспомогательную dll, написанную в Managed С++, чтобы обрабатывать сторону "raw pointer". Мое рассуждение заключается в том, что, используя Unsafe, вы уже выбрасываете множество защит, которые предлагает CLR. Вам может быть легче работать без каких-либо дополнительных средств, испеченных в защите. Не говоря уже о том, что вы можете использовать указатели в полной мере, а затем отбросить их обратно до intptr, когда закончите. Если ничего другого, вы можете захотеть взглянуть на реализацию ToIntPtrArray в С++. Все еще передавайте указатели на стороне С#, но избегайте контроля CLR.

Обратите внимание, что я не говорю, что каждый раз, когда вы используете "небезопасные", вы должны выкинуть С++. Совсем наоборот - небезопасно позволит вам сделать совсем немного, но в этом случае помощник С++, вероятно, должен что-то рассмотреть.

Отказ от ответственности. Я не много сделал с управляемым С++. Может быть, я совершенно не прав, и CLR все равно будет следить за указателями. Если бы какая-то более опытная душа могла прокомментировать и сказать мне в любом случае, было бы очень благодарно.