1

I am unable to pass up an array of structs, but I can pass up ONE.

I have looked at several answers on this site which are indicated as correct. But none of them work for me. In all cases where the answer has the IntPtr as an 'out IntPtr someName' in the PInvoke signature, the value I always get is zero. If the IntPtr is a return value, I cannot resolve it as an array of struct pointers.

Here is my attempt with the IntPtr as a return code: Unamanged C++

extern "C"  EXPORTDLL_API sDeviceEndPoint **CallStartDiscovery(ZBTransport *zbTransport,     int *length, int *result)
{
    if(zbTransport != NULL)
    {
        std::list<sDeviceEndPoint *> deviceEndPoints;
        sDeviceEndPoint **devEndPoints;
        zbTransport->StartDiscovery(&deviceEndPoints, result);
        *length = deviceEndPoints.size();
        if(*length > 0)
        {
            devEndPoints = zbTransport->getDiscoveredEndPointPtrs();
            devEndPoints[*length] = devEndPoints[0]; // Test duplicate
            *length = *length + 1;
            return &devEndPoints[0];
        }
    }
    return NULL;
}

C# PInvoke signature:

    [DllImport("PInvokeBridge.dll", CharSet = CharSet.Unicode)]
    public static extern IntPtr CallStartDiscovery(IntPtr pZbTransportObject, ref int length, ref int result);

C# implementation

    public Collection<DeviceEndPoint> DiscoverHCSensors()
    {
        Collection<DeviceEndPoint> discoveredZigBeeDevices = new Collection<DeviceEndPoint>();
        int length = 0;
        int result = 0;
        IntPtr arrayValue = CallStartDiscovery(_pZbTransportObject, ref length, ref result);

        if (ErrorCodes.ConvertResultCode(result) == ResultCode.rc_SUCCESS)
        {
            var deviceEndPointSize = Marshal.SizeOf(typeof(DeviceEndPoint));
            for (var i = 0; i < length; i++)
            {
                discoveredZigBeeDevices.Add((DeviceEndPoint)Marshal.PtrToStructure(arrayValue, typeof(DeviceEndPoint)));
                arrayValue = new IntPtr(arrayValue.ToInt32() + deviceEndPointSize);
            }
            return discoveredZigBeeDevices;
        }
        else
        {
            String error = "Error"; // Put something helpful in here
            TransportException transEx = new TransportException(error);
            transEx.Method = "DiscoverHCSensors";
            transEx.ErrorCode = result;
            transEx.TransportType = _transportString;
            throw transEx;
        }
    }

I know the layout of my DeviceEndPoint struct defined in C# is correct because if I change the C++ code to pass up just a single pointer to the DeviceEndPoint struct, I correctly load the single struct in the Collection.

The other attempt is to pass the value as an out parameter so in the C++ code I have a parameter sDeviceEndPoint **devs instead.

My C# signature is

    [DllImport("PInvokeBridge.dll", CharSet = CharSet.Unicode)]
    public static extern void CallStartDiscovery(IntPtr pZbTransportObject, out IntPtr devs, ref int length, ref int result);

I have also tried IntPtr[] devs, 'ref' instead of out and they all fail in the same way. The problem is in the C# implementation

int result = 0;
IntPtr arrayValue = IntPTr.Zero;
CallStartDiscovery(_pZbTransportObject, out arrayValue, ref length, ref result);

The value of arrayValue is always null (0). So what I do after this point is not relevant. At least using a return value I get a non null value.

As far as I can tell I am doing what all others are doing. I cannot see what I am doing wrong. Otherwise this is a repeat of a question that has been 'answered' many times in this forum.

Brian Reinhold
  • 2,313
  • 3
  • 27
  • 46
  • You could use a SAFEARRAY (see my answer here for an array of string but that would be the kind of idea: http://stackoverflow.com/questions/15810016/declspecdllexport-vectorstdstring) – Simon Mourier Jun 06 '13 at 15:37

2 Answers2

2

David has the correct result and he explains the reason why my approach did not work. I came to the same conclusion but solved the problem differently. I thought I would provide readable code (the comments are no place for code) in case someone else might use it. I did not test David's code but the following I have used and it works:

IntPtr arrayValue = CallStartDiscovery(_pZbTransportObject, ref length, ref result);
if (result == 0) // If success
{
    if (length > 0 && arrayValue != IntPtr.Zero)
    {
        IntPtr[] devPtrs = new IntPtr[length];
        Marshal.Copy(arrayValue, devPtrs, 0, length); // De-reference once to get array of pointers
        for (var i = 0; i < length; i++)
        {
            // Now marshal the structures
            discoveredZigBeeDevices.Add((DeviceEndPoint)Marshal.PtrToStructure(devPtrs[i], typeof(DeviceEndPoint)));
        }
    }
    return discoveredZigBeeDevices;
}

I used the approach where the IntPtr was returned and is not a parameter (case 1 in original post).

Brian Reinhold
  • 2,313
  • 3
  • 27
  • 46
  • +1 yes, copying to an array of pointers makes the rest of the marshalling easier than IntPtr incrementing. And you are right, this is equivalent to the code in my answer. Thanks for the accept, btw. – David Heffernan Jun 07 '13 at 06:22
1

You are missing a level of indirection. Your native code returns a double pointer. But the managed code only performs a single pointer de-reference. Add in that second layer of indirection and you'll be fine.

Instead of

Marshal.PtrToStructure(arrayValue, typeof(DeviceEndPoint)

you need

Marshal.PtrToStructure(Marshal.ReadIntPtr(arrayValue), typeof(DeviceEndPoint)

And instead of

arrayValue = new IntPtr(arrayValue.ToInt32() + deviceEndPointSize);

you need

arrayValue = new IntPtr(arrayValue.ToInt64() + Marshal.SizeOf(IntPtr));

To be honest though, this is a pretty complex interface. If I was faced with creating an interop layer, I would use C++/CLI.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • I believe you are correct. I came to the same conclusion myself. However I solved it in a slightly different way. I created an IntPtr[] devPtrs = new IntPtr[length]; and then did Marshal.Copy(arrayValue, devPtrs, 0, length); Then I simply did a for-loop of size length using (DeviceEndPoint)Marshal.PtrToStructure(devPtrs[i], typeof(DeviceEndPoint)) to generate the structs from the array of IntPtrs which now had the pointers. After doing this I do not know how the other approaches I have seen here could work. In any case, as far as I am concerned your answer is correct. – Brian Reinhold Jun 07 '13 at 00:38
  • Oh, I am stuck with PInvoke. Windows CE does not support C++/CLI – Brian Reinhold Jun 07 '13 at 00:44