2

I'm trying to convert the following method from C++ to C#

HRESULT GetDevices(
  [in, out]  LPWSTR *pPnPDeviceIDs,
  [in, out]  DWORD *pcPnPDeviceIDs
);

In the MSDN documents it says:

pPnPDeviceIDs [in, out]
A caller-allocated array of string pointers that holds the Plug and Play names of all of the connected devices. To learn the required size for this parameter, first call this method with this parameter set to NULL and pcPnPDeviceIDs set to zero, and then allocate a buffer according to the value retrieved by pcPnPDeviceIDs.

pcPnPDeviceIDs [in, out] On input, the number of values that pPnPDeviceIDs can hold. On output, a pointer to the number of devices actually written to pPnPDeviceIDs.

So far I have this definition:

[PreserveSig]        
int GetDevices(    
    [In, Out] IntPtr pPnPDeviceIDs,
    [In, Out] ref uint pcPnPDeviceIDs
);

I've tried allocating some memory with Marshal.AllocCoTaskMem for pPnPDeviceIDs but I get an AccessViolationException when I try to call the method.

Can anyone show me the correct way to convert LPWSTR * when its an array of string pointers?

EDIT: Here are my definitions:

[ComImport, System.Security.SuppressUnmanagedCodeSecurity,
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("a1567595-4c2f-4574-a6fa-ecef917b9a40")]
public interface IPortableDeviceManager
{
    [PreserveSig]
    HRESULT GetDeviceDescription(
        [In] string pszPnpDeviceID,
        [In, Out] ref StringBuilder pDeviceDescription,
        [In, Out] ref uint pcchDeviceDescription);

    [PreserveSig]
    HRESULT GetDeviceFriendlyName(
        [In] string pszPnPDeviceID,
        [In, Out] ref StringBuilder pDeviceFriendlyName,
        [In, Out] ref uint pcchDeviceFriendlyName);

    [PreserveSig]
    HRESULT GetDeviceManufacturer(
        [In] string pszPnPDeviceID,
        [In, Out] ref StringBuilder pDeviceManufacturer,
        [In, Out] ref uint pcchDeviceManufacturer);

    [PreserveSig]
    HRESULT GetDeviceProperty(
        [In] string pszPnPDeviceID,
        [In] string pszDevicePropertyName,
        [In, Out] IntPtr pData,
        [In, Out] ref uint pcbData,
        [In, Out] ref uint pdwType);

    [PreserveSig]        
    HRESULT GetDevices(     
        [In, Out] IntPtr pPnPDeviceIDs,
        [In, Out] ref uint pcPnPDeviceIDs);


    [PreserveSig]
    HRESULT GetPrivateDevices(
        [In, Out] IntPtr pPnPDeviceIDs,
        [In, Out] ref uint pcPnPDeviceIDs);

    [PreserveSig]
    HRESULT RefreshDeviceList();
}

/// <summary>
/// CLSID_PortableDeviceManager
/// </summary>
[ComImport, Guid("0af10cec-2ecd-4b92-9581-34f6ae0637f3")]
public class CLSID_PortableDeviceManager { }

This is my test code:

var devManager = Activator.CreateInstance(
            typeof(CLSID_PortableDeviceManager)) as IPortableDeviceManager;

        uint pcPnPDeviceIDs = 0;
        var res1 = devManager.GetDevices(IntPtr.Zero, ref pcPnPDeviceIDs);
        // check for errors in res1
        IntPtr ptr = IntPtr.Zero;

        try
        {
            ptr = Marshal.AllocCoTaskMem((int)(IntPtr.Size * pcPnPDeviceIDs));
            var res2 = devManager.GetDevices(ptr, ref pcPnPDeviceIDs);
            // check for errors in res2

            IntPtr ptr2 = ptr;

            for (uint i = 0; i < pcPnPDeviceIDs; i++)
            {
                string str = Marshal.PtrToStringUni(ptr2);
                ptr2 += IntPtr.Size;
            }
        }
        finally
        {
            if (ptr != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(ptr);
            }
        }

        Marshal.ReleaseComObject(devManager);

I get the AccessViolationException on the second call to GetDevices()

megamania
  • 271
  • 3
  • 9
  • Your problem is with the interface... If you try do to a devManager.RefreshDeviceList(); for example everything crashes – xanatos Aug 04 '13 at 11:50
  • Found the problem... Your interface has the wrong method order... How did you generate it? Put GetDevices as the first method. – xanatos Aug 04 '13 at 11:55
  • The correct order of the methods of the interface is: GetDevices, RefreshDeviceList, GetDeviceFriendlyName, GetDeviceDescription, GetDeviceManufacturer, GetDeviceProperty, GetPrivateDevices – xanatos Aug 04 '13 at 12:08

1 Answers1

0

Try this:

uint pcPnPDeviceIDs = 0;
int res1 = GetDevices(IntPtr.Zero, ref pcPnPDeviceIDs);
// check for errors in res1

IntPtr ptr = IntPtr.Zero;

try
{
    ptr = Marshal.AllocCoTaskMem((int)(IntPtr.Size * pcPnPDeviceIDs));
    int res2 = GetDevices(ptr, ref pcPnPDeviceIDs);
    // check for errors in res2

    IntPtr ptr2 = ptr;

    for (uint i = 0; i < pcPnPDeviceIDs; i++)
    {
        string str = Marshal.PtrToStringUni(Marshal.ReadIntPtr(ptr2));
        ptr2 += IntPtr.Size;
    }
}
finally
{
    if (ptr != IntPtr.Zero)
    {
        IntPtr ptr2 = ptr;

        for (uint i = 0; i < pcPnPDeviceIDs; i++)
        {
            Marshal.FreeCoTaskMem(Marshal.ReadIntPtr(ptr2));
            ptr2 += IntPtr.Size;
        }

        Marshal.FreeCoTaskMem(ptr);
    }
}

And I use uint(s) for HRESULT, because they must be parsed bit by bit (and it's easier with unsigned integers)
And allocation of system memory should always be protected by try/finally. And remember to free all the strings :-) (as I have done)

A variant using an IntPtr[]:

The definition of GetDevices (note the use of [MarshalAs(UnmanagedType.LPArray)]:

[PreserveSig]
HRESULT GetDevices(
    [In][MarshalAs(UnmanagedType.LPArray)] IntPtr[] pPnPDeviceIDs,
    [In, Out] ref uint pcPnPDeviceIDs);

The remaining code:

var devManager = Activator.CreateInstance(typeof(CLSID_PortableDeviceManager)) as IPortableDeviceManager;
uint pcPnPDeviceIDs = 0;
HRESULT res1 = devManager.GetDevices(null, ref pcPnPDeviceIDs);
// check for errors in res1

IntPtr[] ptr = null;

try
{
    ptr = new IntPtr[pcPnPDeviceIDs];

    HRESULT res2 = devManager.GetDevices(ptr, ref pcPnPDeviceIDs);
    // check for errors in res2

    for (uint i = 0; i < pcPnPDeviceIDs; i++)
    {
        string str = Marshal.PtrToStringUni(ptr[i]);
    }
}
finally
{
    if (ptr != null)
    {
        for (uint i = 0; i < pcPnPDeviceIDs; i++)
        {
            Marshal.FreeCoTaskMem(ptr[i]);
        }
    }
}

In this way you don't have to allocate the main array in unmanaged memory.

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • I'm still getting the AccessViolationException. I'll edit my post to show all my code so you can reproduce the problem. Thanks – megamania Aug 04 '13 at 11:26
  • @user2648561 Where? On the first call or on the second call? Or in the various PtrToStringUni? – xanatos Aug 04 '13 at 11:27
  • @user2648561 If you fix the interface by putting GetDevices as first and you modify the Marshal.PtrToStringUni it works (I added a Marshal.ReadIntPtr). – xanatos Aug 04 '13 at 11:58
  • @user2648561 Fixed even the deallocation of the strings. – xanatos Aug 04 '13 at 12:06
  • I didn't realize you had to define the methods in the same order as the original header file! You learn something new everyday! Thanks. – megamania Aug 04 '13 at 12:15
  • @user2648561 The two important things are the order and that they must all be present – xanatos Aug 04 '13 at 12:17
  • I've just done pretty much the same thing to deallocate the strings. BTW, I don't suppose you know what the value of WPD_MESSAGE is? I want my application to look for WPD events but I can't find the value in the Windows SDK anywhere. – megamania Aug 04 '13 at 12:20
  • @user2648561 Sadly I don't do COM :-) I have added a variant of the code that is a little easier (it uses `IntPtr[]`) – xanatos Aug 04 '13 at 12:26
  • Yes, thats much more elegant – megamania Aug 04 '13 at 12:30