views:

696

answers:

3

I am working on a system that requires interaction with a native C API using P/Invoke. Now I've (yet again) stumbled upon a problem which I cannot seem to solve in any way. The original function is designed to return 2 kinds of structures, based on a parameter that specifies which structure to use.

The C header file defines the structures and function as follows:

#pragma pack(1)
typedef struct {
   DWORD JobId; 
   DWORD CardNum;
   HANDLE hPrinter;
} CARDIDTYPE, FAR *LPCARDIDTYPE;
#pragma pack()

typedef struct {
   BOOL        bActive;
   BOOL        bSuccess;
} CARD_INFO_1, *PCARD_INFO_1, FAR *LPCARD_INFO_1;

typedef struct {
   DWORD       dwCopiesPrinted;
   DWORD       dwRemakeAttempts;
   SYSTEMTIME  TimeCompleted;
} CARD_INFO_2, *PCARD_INFO_2, FAR *LPCARD_INFO_2;

BOOL ICEAPI GetCardId(HDC hdc, LPCARDIDTYPE pCardId);
BOOL ICEAPI GetCardStatus(CARDIDTYPE CardId, DWORD level, LPBYTE pData, DWORD cbBuf, LPDWORD pcbNeeded );

I have attempted to implement P/Invoke wrappers like this:

[StructLayout(LayoutKind.Sequential, Pack=1)]
public class CARDIDTYPE {
    public UInt32 JobId;
    public UInt32 CardNum;
    public IntPtr hPrinter;
}

[StructLayout(LayoutKind.Sequential)]
public class CARD_INFO_1 {
    public bool bActive;
    public bool bSuccess;
}

[StructLayout(LayoutKind.Sequential)]
public class CARD_INFO_2 {
    public UInt32 dwCopiesPrinted;
    public UInt32 dwRemakeAttempts;
    public Win32Util.SYSTEMTIME TimeCompleted;
}
[DllImport("ICE_API.DLL", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
public static extern bool GetCardId(HandleRef hDC, [Out]CARDIDTYPE pCardId);

[DllImport("ICE_API.DLL", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
public static extern bool GetCardStatus(CARDIDTYPE CardId, UInt32 level, [Out] byte[] pData, UInt32 cbBuf, out UInt32 pcbNeeded);

Calling the "GetCardId" seems to work fine. I get plausible data in CARDIDTYPE instance after calling it. However when I call "GetCardStatus" the problems start. The type of structure that should be returned is defined by the "level" param, and a value of 1 should result in a CARD_INFO_1 structure to be returnes in "pData".

The documentation contains the following C example:

CARD_INFO_1 ci1;
DWORD cbNeeded;
ci1.bActive = TRUE;
if (GetCardStatus(*lpCardID, 1, (LPBYTE)&ci1, sizeof(ci1), &cbNeeded )) { /* success */ }

My equivalent C# implementation is like this:

uint needed;
byte[] byteArray = new byte[Marshal.SizeOf(typeof(CARD_INFO_1))];
if (GetCardStatus(cardId, 1, byteArray, (uint)byteArray.Length, out needed)) { /* success */ }

When I execute this C# code, the method returns false and Marshal.GetLastWin32Error() return -1073741737 (which does not make much sense to me). I see no reason why this call should fail, and definitely not with this error code. So I suspect I have got something wrong in my P/Invoke wrapper.

I know that using "byte[]" as the type of pData is probably not correct, but according to some googling a "LPBYTE" translates to "[Out] byte[]". I guess the correct way to do this is to have pData as an IntPtr, and create the structure using Marshal.PtrToStructure(...). I have tried this, but the result is the same. Here is the code for this scenario:

[DllImport(@"ICE_API.DLL", CharSet = CharSet.Auto, EntryPoint = "_GetCardStatus@28", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
public static extern bool GetCardStatus(CARDIDTYPE CardId, UInt32 level, IntPtr pData, UInt32 cbBuf, out UInt32 pcbNeeded);

uint needed;
int memSize = Marshal.SizeOf(typeof(CARD_INFO_1));
IntPtr memPtr = Marshal.AllocHGlobal(memSize);
if (!GetCardStatus(cardId, 1, memPtr, (uint)memSize, out needed)) {
    int lastError = Marshal.GetLastWin32Error();
    // error code is -1073741737
}
CARD_INFO_1 info = (CARD_INFO_1)Marshal.PtrToStructure(memPtr, typeof(CARD_INFO_1));
Marshal.FreeHGlobal(memPtr);

Edit: One thing I forgot to mention is that for some reason the GetCardStatus call fails with an unknown entry point exception if I do not specify EntryPoint = "_GetCardStatus@28". This has not happened to any other function I have wrapped, so it got me wondering a bit.

+1  A: 

The problem is you are using [Out] where you should be using nothing

[DllImport("ICE_API.DLL", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
public static extern bool GetCardStatus(CARDIDTYPE CardId, UInt32 level, byte[] pData, UInt32 cbBuf, out UInt32 pcbNeeded);

The Out/In attributes tell the CLR Marshaller in which direction the immediate variable will be marshalled. In the case of the byte[], the parameter is really doing nothing. One of it's sub elements is being moved.

Marshalling arrays is a tricky business though, especially when used directly in a signature vs. a structure. It may be better for you to use an IntPtr, allocate memory there and manually marshal the array out of the IntPtr.

[DllImport("ICE_API.DLL", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
public static extern bool GetCardStatus(CARDIDTYPE CardId, UInt32 level, IntPtr pData, UInt32 cbBuf, out UInt32 pcbNeeded);

public void Example(uint size) {
  // Get other params
  var ptr = Marshal.AllocHGlobal(size);
  GetCardStatus(cardId, level, ptr, size, out needed);
  // Marshal back the byte array here
  Marshal.FreeHGlobal(ptr);
}
JaredPar
I have tries this, but the result is the same. Function retuns false, and the error code is the same.
Johnny Egeland
@Johnny, the first or second solution?
JaredPar
Sorry... The second solution. I will edit my post to include this code.
Johnny Egeland
@Johhny, what does the needed parameter return? Is it bigger than the size you used for the array?
JaredPar
Not sure I understand what you mean. The returned structure simply contains 2 booleans. According to the example code and documentation, the returned data should match the size of the CARD_INFO_! structure.
Johnny Egeland
@Johhny, the pcbNeeded variable is also telling you how long the array needs to be in case the size of the byte array wasn't long enough to hold the data. It's common in C APIs to return false and a bigger size when the array isn't big enough.
JaredPar
Ah, I see. The needed value is 0 after the function call. I have called other functions who uses this convention, and this have worked fine. Then the needed value returned corresponds to the required byte size of the returned data.
Johnny Egeland
I have tried calling the method with a IntPtr.Zero value and 0 as the size, to inspect the resulting "needed" value. It still is 0.
Johnny Egeland
@Johhny, if that's the case then one of the other parameters is almost certainly invalid. Have you broken in both native and managed code to assert the CardId value has the exact same values? The packing makes me suspicious but only because I have little experience with it.
JaredPar
I do not have control over the Unmanaged part here, since it's a third party driver interface. Is there any other way to check this?I also have no experience with Packing, and I only adopted the parameter due to the #pragma in the original code. I have tried without this, and there is no difference
Johnny Egeland
@Johnny, the only other way I can think to play with this is to write a small C/C++ program that calls the function. You can use that program to verify what the data looks like in native code and verify the managed code has the same values. Another trick is to write your own dll with similar
JaredPar
(cont) structures and call into it to see how pack affects the struct. It's pretty tedious but it will rule out the #pragma pack being the issue.
JaredPar
+2  A: 

_GetCardStatus@28 gave me an idea. Unless you are running on 64-bit Windows, you've got the number of arguments wrong. Your P/Invoke for GetCardStatus would be _GetCardStatus@20, because it has 5 32-bit arguments. Your C declaration of GetCardStatus seems to accept the cardId by value rather than by reference. Since CARDIDTYPE is 12 bytes long, this would give the correct length of the argument list (28). Moreover, this would explain both your receiving an error code of -1073741737 (C0000057, STATUS_INVALID_PARAMETER) — since you're not passing a valid cardIdand the access violation — GetCardStatus tries to write to pcbNeeded, which is garbage because the marshaler hasn't even pushed it!

Ergo:

[DllImport("ICE_API.DLL", CharSet = CharSet.Auto, 
     CallingConvention = CallingConvention.Winapi, SetLastError = true)]
 public static extern bool GetCardStatus (
     IntPtr hPrinter, UInt32 cardNum, UInt32 jobId, UInt32 level, 
    [In, Out] CARD_INFO_1 data, UInt32 cbBuf, out UInt32 pcbNeeded) ;
[DllImport("ICE_API.DLL", CharSet = CharSet.Auto, 
     CallingConvention = CallingConvention.Winapi, SetLastError = true)]
 public static extern bool GetCardStatus (
     IntPtr hPrinter, UInt32 cardNum, UInt32 jobId, UInt32 level, 
    [In, Out] CARD_INFO_2 data, UInt32 cbBuf, out UInt32 pcbNeeded) ;

Note reverse order of the three CARDIDTYPE members: stdcall pushes the parameters left-to-right (i.e. towards lower addresses), and my guess is that a struct is "pushed" as a unit.

Also, if you later close the printer handle with CloseHandle, I'd suggest receiving the handle in CARDIDTYPE into an appropriate SafeHandle, not into a bare IntPtr, and declaring GetCardStatus to receive the safe handle.

Anton Tykhyy
This was actually what I did at first. Using the signatures as you describe here results in an exception:System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
Johnny Egeland
I read your question more carefully and edited my answer.
Anton Tykhyy
Thanks for your feedback. Your answer led me to solving the problem eventually. I will post the complete solution in a new answer :-)
Johnny Egeland
+1  A: 

As Anton suggests the problem lies in the parameters passed to the function. I did not notice this yesterday but the CARDIDTYPE structure is passed by pointer in the GetCardID function, and by value in the GetCardStatus function. In my calls I passed the CARDIDTYPE by pointer to the GetCardStatus also, forcing the P/Invoke framework to locate the correct function by specifying the exact function name as found in Dependecy Walker.

I solved this by defining the CARDIDTYPE as a struct instead of a class, and pass it by reference to the GetCardId function. Further the CARDIDTYPE is marshaled as a Struct when passed to the GetCardStatus function. This in addition to Antons technique of using two function definitions with different pData types (CARD_INFO_1 and CARD_INFO_2) now works correctly. Here is the final definitions:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct CARDIDTYPE {
 public UInt32 JobId;
 public UInt32 CardNum;
 public IntPtr hPrinter;
}

[StructLayout(LayoutKind.Sequential)]
public class CARD_INFO_1 {
 public bool bActive;
 public bool bSuccess;
}

[StructLayout(LayoutKind.Sequential)]
public class CARD_INFO_2 {
 public UInt32 dwCopiesPrinted;
 public UInt32 dwRemakeAttempts;
 public Win32Util.SYSTEMTIME TimeCompleted;
}

[DllImport("ICE_API.DLL", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
public static extern bool GetCardId(HandleRef hDC, ref CARDIDTYPE pCardId);

[DllImport(@"ICE_API.DLL", EntryPoint = "GetCardStatus", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
public static extern bool GetCardStatus([MarshalAs(UnmanagedType.Struct)]CARDIDTYPE CardId, UInt32 level,
 [In, Out] CARD_INFO_1 pData, UInt32 cbBuf, out UInt32 pcbNeeded);

[DllImport(@"ICE_API.DLL", EntryPoint = "GetCardStatus", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
public static extern bool GetCardStatus([MarshalAs(UnmanagedType.Struct)]CARDIDTYPE CardId, UInt32 level,
 [In, Out] CARD_INFO_2 pData, UInt32 cbBuf, out UInt32 pcbNeeded);

Thank you both for your contribution to solving this problem :-)

Johnny Egeland
Heh, I didn't know about UnmanagedType.Struct. Thanks for the tip! :)
Anton Tykhyy