10

I'm stuck on a seemingly trivial task and need your help.

I need to write a method with the following signature:

System.Array ToIntPtrArray(System.Array a)

where an actual argument can be an array of any pointer type (e.g. int*[], long**[], void*[,]) and returning an array of the same shape with elements of type System.IntPtr having the same numeric values as elements of an input array. The problem is that I do not understand how to extract numeric values of pointers if I do not know their types beforehand.


For example, if I knew beforehand that my argument is always of type void*[], I could write the method as follows:

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

But the problem is it could be not void*[], but void**[] or anything else, and the method should be able to handle all cases.

Peter Smolensky
  • 101
  • 1
  • 5
  • 1
    What is the numerical value of `void*`? – Cédric Bignon Jul 23 '13 at 17:30
  • Try explicit type conversion, although i'm sure your function will have lot of undefined behaviour. – nj-ath Jul 23 '13 at 17:34
  • 4
    @CédricBignon I do not quite understand your question. A variable of type `void*` could have any numeric value within the platform-dependent range of addresses, and in any case it would fit into `IntPtr` on that platform. I added an example showing how it should work in this case. – Peter Smolensky Jul 23 '13 at 17:50
  • How many pointer types are you expecting? Could you not accomplish this by overloading the function. I cannot find any way to accomplish this with a `System.Array` object, as calling `.GetValue` throws an exception, and indexing is not possible either. – nicholas Jul 23 '13 at 17:52
  • 2
    @TheJoker Yes, I would use an explicit conversion if I knew an exact type of a pointer at compile-time. But as i do not know it, what conversion do you suggest to use? And what kind of undefined behavior do you expect? – Peter Smolensky Jul 23 '13 at 17:54
  • How about function overloading with the same return type? – nj-ath Jul 23 '13 at 17:57
  • 3
    @nicholas The method should be able to handle all types of pointers that could possibly exist in the .NET runtime. I do not know what possible callers may choose to pass as an argument. I could use a set of overloads provided that they would cover all possible inputs. – Peter Smolensky Jul 23 '13 at 17:57
  • @TheJoker Could you please be more specific? – Peter Smolensky Jul 23 '13 at 18:00
  • Is performance and perfect static typing important? If not reflection to the rescue. – usr Jul 23 '13 at 19:02
  • 1
    Something to watch out for (from the MSDN docs(: "Using an Array object of pointers in native code is not supported and will throw a NotSupportedException for several methods." GetValue is one such method. – Xcelled Jul 23 '13 at 19:39
  • Have you tried `Buffer.BlockCopy`? – Ben Voigt Jul 23 '13 at 21:29
  • @BenVoigt BlockCopy cannot be used with pointers. Shame, I got real excited for a moment. – Xcelled Jul 23 '13 at 23:03
  • @PeterSmolensky, I also feel it is obligatory to ask the common question "why would you want to do this" anyway? C# as a language is not really meant to use pointer arithmetic except in rare cases. You desire a function that takes arrays of pointers, but where are these arrays being generated anyway? Every single pointer that sits in those arrays has to be fixed explicitly. Are you doing this by marshaling or by the scope-limited `fixed` keyword? – nicholas Jul 25 '13 at 21:33

3 Answers3

3

The short answer is, this cannot be done directly. The reasons are that if you pass your conversion function any of the conventional index-capable containers (System.Array, Collections.IList, ArrayList, etc.) performing the index operations will attempt to cast the result to System.Object. Pointers in C# do not derive from Object, so this will result in an SystemNotSupported or similar exception.

There are two reasonable workarounds:

  1. Convert the pointer arrays to void pointer arrays before calling the method.
  2. Convert the pointer arrays to void pointer pointers before calling the method.

The first one is rather cumbersome, as it requires duplicating the entire contents of the array with a for loop. The second option requires passing in the length of the array as it is no longer wrapped with a managed System.Array object.

Sample Code

Method:

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

Sample Usage (integer pointer array):

int*[] intPtrArray;

// Code that initializes the values of intPtrArray

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

Sample Usage (void pointer pointer array):

void**[] voidPtrPtrArray;

// Code that initializes the values of voidPtrPtrArray

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

Sample Usage (multidimensional int pointer array):

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

Where TotalSize and ReshapeArray are helper functions that are written to deal with multi-dimensional arrays. For tips on how to accomplish this see: Programatically Declare Array of Arbitrary Rank.

Community
  • 1
  • 1
nicholas
  • 2,969
  • 20
  • 39
  • Nice, but it does not handle the OP's case of `void*[,]` with the same shape correctly. It flattens the array into one dimension but that's unavoidable without the use of reflection, I believe. – Xcelled Jul 23 '13 at 19:52
  • @Xcelled194, I would challenge that even without pointer constraints of the problem, creating an arbitrary array with the same rank and dimensions as a an original array is a challenging problem. For jagged arrays this can be accomplished with recursion, for rectangular multidimensional arrays, I am not sure it can be done at all. – nicholas Jul 23 '13 at 20:02
  • It would have to be done with reflection and SetValue(), I'm almost certain. But my gut tells me it could be done. – Xcelled Jul 23 '13 at 21:01
  • Yes, I just wrote the code to create a rectangular multidim array. Setting values would need to use SetValue method, but can be done. ` var a = new byte[10, 3]; int[] lens = new int[a.Rank], lbs = new int[a.Rank]; for (int i = 0; i < a.Rank; i++) { lens[i] = a.GetUpperBound(i)+1; lbs[i] = a.GetLowerBound(i); } var b = (byte[,])Array.CreateInstance(typeof(byte), lens, lbs);` – Xcelled Jul 23 '13 at 21:14
  • Multi-dimensional arrays are stored linearly, so the main method `IntToPtrArray` will still work. To keep things simple I left the `ReshapeArray` function as an external helper; however, it could very well be implemented inside the `IntToPtrArray` function as well. Jagged arrays would be more difficult. – nicholas Jul 23 '13 at 21:20
  • @Xcelled194 I'm OK with reflection for the general case. If I have to optimize, I will create specializations for most common cases using explicit types. – Peter Smolensky Jul 25 '13 at 15:59
2

This is a rather difficult problem. Creating an array of the proper shape isn't too bad.

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

That creates an array of the right type and shape (dimensions and length), but it has a problem. The GetValue method, which you use to get an item from the array, returns an object. And you can't cast a pointer type to an object. No way, no how. So you can't get the value from the array! If you call GetValue on an array of long*, for example, you'll get "type not supported."

I think you need some way to copy that oddly-shaped array to a one-dimensional array of int* (or any other pointer type). Then you could directly index the temporary array and get the values to populate your IntPtr array.

It's an interesting chicken-and-egg problem. If you pass it as a System.Array, then you can't get items from it because there's no conversion path from object to int* (or any other pointer type). But if you pass it as a pointer type (i.e. int**), then you can't get the shape of the thing.

I suppose you could write it as:

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

You then have the System.Array metadata and the actual data in a form that you can use.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • Your next to last suggestion is the one that would really work. The final suggestion of just passing a pointer to the array would not since you are not allowed to take the address of a `System.Array` object (unless you want to do Marshalling). Pinning the array as a `void**` with the `fixed` keyword and also passing the original array for metadata is only way to kill all the OP's requests with one stone. – nicholas Jul 23 '13 at 21:35
  • @nicholas: Thanks. You're right. I did a little more exploration. See my update. – Jim Mischel Jul 23 '13 at 22:26
0

Even though the question has been answered well, I feel the need to leave a note/warning to future readers.

The CLR is designed to keep you safe, or at least as safe as possible. It accomplishes this with (among other things) type safety and abstracting memory operations. While you can turn some of these protections off with the unsafe block, some protections are hardcoded into the compiler/runtime. In order to circumvent this additional hurdle, one must resort to some hackey, and possibly slow, code. While these methods work, experience has taught me that doing such things leads to problems down the road, whether it be a change in the runtime, a future programmer needing to modify that segment of code, or you yourself needing to do something else with those pointers later on in the code.

At this point, I would seriously consider a helper dll written in Managed C++ to handle the "raw pointer" side of things. My reasoning is that by using Unsafe, you're already throwing out many protections the CLR offers. You may find it easier to work unencumbered by any additional, baked in protections. Not to mention you can use pointers to their full extent, and then cast them back to intptr when you're done. If nothing else, you may want to look at implementing ToIntPtrArray in C++. Still pass it pointers on the C# side, but escape the CLR's oversight.

Note that I'm not saying that every time you use "unsafe" you should bust out the C++. Quite contrary - unsafe will allow you to do quite a bit - but in this instance, a C++ helper is probably something to consider.

Disclaimer: I have not done a whole lot with managed C++. It could be that I am totally wrong and the CLR would still monitor the pointers. If some more experienced soul could comment and tell me either way, It'd be much appreciated.

Xcelled
  • 2,084
  • 3
  • 22
  • 40