Search code examples
c#winapipinvokemarshallingsetupapi

Marshalling SetupGetInfInformation from C++ to C#


I'm trying to marshal the SetupGetInfInformation function from Windows' SetupAPI to C#.

I've defined the (marshalled) structures necessary as follows:

    internal const uint INFINFO_INF_NAME_IS_ABSOLUTE = 2;

    [StructLayout(LayoutKind.Sequential)]
    internal struct SP_INF_INFORMATION
    {
        public uint InfStyle;
        public uint InfCount;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
        public byte[] VersionData;
    }

    [DllImport("setupapi.dll", SetLastError = true)]
    internal static extern bool SetupGetInfInformation(
        [In] string InfSpec,
        [In] uint SearchControl,
        //[In, Out] ref SP_INF_INFORMATION ReturnBuffer,
        [In, Out] ref IntPtr ReturnBuffer,
        [In] uint ReturnBufferSize,
        [In, Out] ref uint RequiredSize
        );

And then I'm attempting to utilize the function, firstly by passing 0 for the ReturnBufferSize argument, to request the size buffer necessary:

IntPtr ip = new IntPtr();

bool result = SetupAPI.SetupGetInfInformation(
    @"D:\TestDriverFile\intcoed.inf",
    SetupAPI.INFINFO_INF_NAME_IS_ABSOLUTE, // 2
    ref ip,
    0,
    ref i
    );

This works successfully and consistently returns 422 for this specific INF, so I'm confident this much is working properly.

However, the next step (allocating the buffer appropriately and calling the function a second time to fill the buffer with the requested data) is where I'm having difficulty.

Currently, I'm trying this (following the above):

ip = Marshal.AllocHGlobal((int)i);

result = SetupAPI.SetupGetInfInformation(
    @"D:\TestDriverFile\intcoed.inf",
    SetupAPI.INFINFO_INF_NAME_IS_ABSOLUTE,
    ref ip,
    i,
    ref i
    );

This consistently crashes vshost.exe with an "vshost32.exe has stopped working" dialog, which allows me to Debug or Close program, and no other helpful information.

I have also tried changing the marshalled SetupGetInfInformation function signature to reflect SP_INF_INFORMATION (instead of IntPtr, see the commented-out line above), and in doing so can still consistently call SetupGetInfInformation the first time and receive 422. I then attempted to allocate enough space in a buffer for it, and pass that in the second call, as follows:

SetupAPI.SP_INF_INFORMATION buf = new SetupAPI.SP_INF_INFORMATION();

// Make the first call, passing in ref buf, receive 422 as a response.

buf.VersionData = new byte[i];

result = SetupAPI.SetupGetInfInformation(
    @"D:\TestDriverFile\intcoed.inf",
    SetupAPI.INFINFO_INF_NAME_IS_ABSOLUTE,
    ref buf,
    i,
    ref i
    );

This also crashes vshost.exe in the same fashion as above.

It seems apparent to me that I'm not allocating a proper buffer for the second call, but I might be missing other required items as well.

Would someone mind pointing me in the right direction? I haven't found any assistance on this specific function here on StackOverflow yet, and searching for properly marshalling variable-sized arrays and using the Marshal to allocate/shift memory has been helpful (and educating), but hasn't gotten me past this problem.

EDIT:

Thanks to Dave Cluderay and Simon Mourier. I've accepted Simon's solution as the answer, but wanted to provide my finished code to show (completely) how to marshal SetupGetInfInformation and to release unmanaged memory:

    [StructLayout(LayoutKind.Sequential)]
    public struct SP_INF_INFORMATION
    {
        public int InfStyle;
        public int InfCount;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
        public byte[] VersionData;
    }

    [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    public static extern bool SetupGetInfInformation(
        string InfSpec,
        int SearchControl,
        IntPtr ReturnBuffer,
        int ReturnBufferSize,
        ref int RequiredSize
        );

    public static void SetupGetInfInformation_NET(
        string infPath,
        ref SP_INF_INFORMATION infInfo
        )
    {
        infInfo = new SP_INF_INFORMATION();
        int size = 0;
        IntPtr ip = new IntPtr();

        try
        {
            if (!SetupAPI.SetupGetInfInformation(infPath, SetupAPI.INFINFO_INF_NAME_IS_ABSOLUTE, IntPtr.Zero, 0, ref size))
                throw new Exception("Error calling SetupGetInfInformation() for required buffer size.", new Win32Exception(Marshal.GetLastWin32Error()));

            if (size == 0)
                return;

            ip = Marshal.AllocHGlobal(size);

            if (!SetupAPI.SetupGetInfInformation(infPath, SetupAPI.INFINFO_INF_NAME_IS_ABSOLUTE, ip, size, ref size))
                throw new Exception("Error calling SetupGetInfInformation() to retrieve INF information.", new Win32Exception(Marshal.GetLastWin32Error()));

            infInfo.InfStyle = Marshal.ReadInt32(ip, 0);    // The first 4-byte int is for InfStyle.
            infInfo.InfCount = Marshal.ReadInt32(ip, 4);    // The second 4-byte int is for InfCount.

            // Marshal the data from the unmanaged buffer to a managed buffer.
            byte[] buf = new byte[size];
            Marshal.Copy(ip, buf, 0, size);

            // Initialize VersionData to be large enough to hold the VersionData from the managed buffer. We remove 8 bytes (4 for InfStyle, 4 for InfCount.)
            infInfo.VersionData = new byte[size - 8];

            // Copy the VersionData from the managed buffer into infInfo.VersionData, offsetting 8 bytes for InfStyle and InfCount.
            Array.Copy(buf, 8, infInfo.VersionData, 0, size - 8);
        }
        finally
        {
            Marshal.FreeHGlobal(ip);
        }
    }

Solution

  • This kind of structure is of variable size (because of the last member). You must call the API once to get the size, and a second time with the properly allocated buffer.

    Here is a definition that works:

    int size = 0;
    if (!SetupGetInfInformation(@"D:\TestDriverFile\intcoed.inf", INFINFO_INF_NAME_IS_ABSOLUTE,
        IntPtr.Zero, 0, ref size)) // pass NULL the first time
        throw new Win32Exception(Marshal.GetLastWin32Error());
    
    // now, size contains the required buffer size
    var ptr = Marshal.AllocHGlobal(size);
    if (!SetupGetInfInformation(@"D:\TestDriverFile\intcoed.inf", INFINFO_INF_NAME_IS_ABSOLUTE,
        ptr, size, ref size))
        throw new Win32Exception(Marshal.GetLastWin32Error());
    
    // now, ptr contains a pointer to a SP_INF_INFORMATION structure
    var InfStyle = Marshal.ReadInt32(ptr);
    var InfCount = Marshal.ReadInt32(ptr, 4);
    ... etc...
    
    [DllImport("setupapi.dll", SetLastError = true)]
    internal static extern bool SetupGetInfInformation(
        string InfSpec,
        int SearchControl,
        IntPtr ReturnBuffer,
        int ReturnBufferSize,
        ref int RequiredSize
        );