1

I am trying to create C# code which calls a function in a DLL with a C interface. I am a C and C++ expert but my knowledge of C# is pretty limited. I have already managed to load the DLL into the C# program and to call functions whose interfaces use simple data types like int or even strings. But this one exceeds my abilities, and I hope for some helpful hints.

The DLL declares this entry point:

BOOL WINAPI GetUsers(ALLUSERINFO* pInfo, DWORD size, DWORD* count);

where pInfo points at a C array of structs with size elements which the GetUsers function fills with data. count receives the number of array elements which were actually filled. Both pInfo and count are out-only.

ALLUSERINFO is defined as

struct ALLUSERINFO : public USER_INFO2, public PRINCIPAL_INFO
{
    WCHAR Name[10];
    WCHAR Role[256];
};

and its bases classes are

struct USER_INFO2
{
    WCHAR Name[80];
    WCHAR Password[40];
};
struct PRINCIPAL_INFO
{
    int PrincipalStatus;
    WCHAR UniqueID[39];
};

(WCHAR is a typedef for wchar_t.)

I assume I must turn the base classes into embedded members of ALLUSERINFO, but I am completely at loss regarding how to create an array of structs which contain fixed-size C strings in C# and then pass this array by pointer to its first element.

I found the StructLayout and MarshalAs attributes but from the docs which I read I am not even sure whether they are suitable in my case, much less how they work when it comes to an array of structs of structs, as in this case.

Thank you!

h.s.
  • 139
  • 9

2 Answers2

2

Your definitions are as follows:

  • C# doesn't support struct inheritance, you need to put them as fields or properties instead.
  • Use UnmanagedType.ByValTStr and SizeConst for inline WCHAR arrays. make sure to set CharSet = CharSet.Unicode on the struct.
[DllImport("yourdll.dll")]
static extern bool GetUsers(
  [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] ALLUSERINFO[] pInfo,
  int size,
  [Out]out int count);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct ALLUSERINFO
{
    public USER_INFO2 UserInfo;
    public PRINCIPAL_INFO PrincipalInfo;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 10)]
    public string Name;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
    public string Role;
};

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct USER_INFO2
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
    public string Name;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 40)]
    public string Password;
};

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct PRINCIPAL_INFO
{
    public int PrincipalStatus;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 39)]
    public string UniqueID;
};

You then simply call it like this

var buffer = new ALLUSERINFO[100];
if(!GetUsers(buffer, buffer.Length, out var count))
    throw new Win32Exception(Marshal.GetLastWin32Error());
Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • Hi @Charlieface, thank you for this explanation. Unfortunately, now I keep getting an access violation in the C++ code when I try to write to the buffer. I am wondering whether `ref buffer` passes the address of the variable 'buffer' into the DLL, rather than the address of the result of the 'new'. Or should I use something like `Marshal.AllocHGlobal()` to allocate the buffer? – h.s. May 19 '23 at 15:01
  • Sorry that's a mistake it shouldn't be `ref` as the array is already a pointer, `ref` will just make it a pointer to a pointer – Charlieface May 19 '23 at 16:58
  • Thanks @Charlieface, now the access violation is gone. However, after GetUsers returns, the buffer is empty. The C declaration of GetUsers is in my original post. The C# declaration is (omitting namespace and class): `[DllImport("mydll.dll", CharSet = CharSet.Unicode)] internal static extern bool GetUsers(ALLUSERINFO[] pInfo, uint maxUsers, out uint count);` I tried using the `ref` and `out` keywords with the `pInfo` parameter, but when I tried that, the compiler wanted me to repeat the keyword in the call, which resulted in the access violation. – h.s. May 22 '23 at 12:08
  • Are you setting the `count` parameter before returning from C? – Charlieface May 22 '23 at 12:11
  • The C code sets both the array pointed to by pInfo and the count parameter. Even if not successful, count is set to 0. In a debug session, I saw count was set to 11. This is the value I would expect. – h.s. May 22 '23 at 12:15
  • If it's setting the pointer then weird things will happen. It should just use the existing buffer you pass in, as I show in the code above. If you wanted to pass your own buffer then you need to create it on the correct heap, and needs to be a pointer to a pointer `ALLUSERINFO** pInfo` which is rather messy. – Charlieface May 22 '23 at 12:17
  • You misunderstood me, or maybe I was unclear. The C code treats pInfo as a pointer to an array of structs, and it writes into these structs. It does not write into the pointer itself. – h.s. May 22 '23 at 13:51
0

Finally I found a solution. It may not be elegant but it works.

I needed the following C# declarations:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal unsafe struct USER_INFO2
{
    public fixed char Name[80];
    public fixed char Password[40];
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal unsafe struct PRINCIPAL_INFO
{
    internal int PrincipalStatus;
    public fixed char UniqueID[39];
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal unsafe struct ALLUSERINFO
{
    internal USER_INFO2 UserInfo;
    internal PRINCIPAL_INFO PrincipalInfo;
    public fixed char Name[10];
    public fixed char Role[256];
}

[DllImport("mydll.dll", CharSet = CharSet.Unicode)]
internal static extern bool GetUsers(ALLUSERINFO* pInfo, uint size, out uint count);

Then the call to GetUsers looks like this:

unsafe
{
    ALLUSERINFO[] buffer = new ALLUSERINFO[maxUsers];
    fixed (ALLUSERINFO* pBuffer = &buffer[0])
    {
        uint actualUsers;
        bool res = DSQL.DSEnumAllUsers(pBuffer, maxUsers, out actualUsers);
        if (!res)
            return false;
        // do something with the returned data in the buffer
    }
}

I am not sure whether all the attributes are required in the struct declarations. The crucial thing seems to be using the unsafe and fixed keywords.

Thanks to @Charlieface who helped me direct my thoughts into the correct direction!

h.s.
  • 139
  • 9