tags:

views:

1955

answers:

4

I am trying to write a C# P/Invoke wrapper for a C API (a native Win dll), and generally this is working fine. The only exception is a specific method which takes a struct as a parameter in the C code. The function is invoked without any exceptions, but it returns false indicating that something failed in the execution.

In the API header file the involved method and structs are defined as follows:

#define MAX_ICE_MS_TRACK_LENGTH  256
typedef struct tagTRACKDATA
{   
    UINT nLength;
    BYTE TrackData[MAX_ICE_MS_TRACK_LENGTH];
} TRACKDATA, FAR* LPTRACKDATA;
typedef const LPTRACKDATA LPCTRACKDATA;

BOOL ICEAPI EncodeMagstripe(HDC /*hDC*/,
       LPCTRACKDATA /*pTrack1*/,
       LPCTRACKDATA /*pTrack2*/,
       LPCTRACKDATA /*pTrack3*/,
       LPCTRACKDATA /*reserved*/);

I have made an attempt to create a C# P/Invoke wrapper using the following code:

public const int MAX_ICE_MS_TRACK_LENGTH = 256;

[StructLayout(LayoutKind.Sequential)]
public class MSTrackData {
    public UInt32 nLength;
    public readonly Byte[] TrackData = new byte[MAX_ICE_MS_TRACK_LENGTH];
}

[DllImport("ICE_API.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool EncodeMagstripe(IntPtr hDC,
                    [In]ref MSTrackData pTrack1,
                    [In]ref MSTrackData pTrack2,
                    [In]ref MSTrackData pTrack3,
                    [In]ref MSTrackData reserved);

Then I try to invoke the EncodeMagstripe method using the following C# code:

CardApi.MSTrackData trackNull = null;
CardApi.MSTrackData track2 = new CardApi.TrackData();
byte[] trackBytes = Encoding.ASCII.GetBytes(";0123456789?");
track2.nLength = (uint)trackBytes.Length;
Buffer.BlockCopy(trackBytes, 0, track2.TrackData, 0, trackBytes.Length);

if (!CardApi.EncodeMagstripe(hDC, ref trackNull, ref track2, ref trackNull, ref trackNull)) {
    throw new ApplicationException("EncodeMagstripe failed", Marshal.GetLastWin32Error());
}

This causes an ApplicationException to be thrown, and the error code is 801 which according to the documentation means "Data includes too many characters for the selected Track 2 format.". However the selected track format should allow up to 39 characters (I have also tried shorter strings).

I suspect the problem occurrs due to something I did wrong in the MSTrackData definition, but I cannot see what this may be. Does anyone have any suggestions?

+2  A: 

I would define the BYTE array not with new, but use the following code instead to initialise the right size:

[MarshalAs(UnmanagedType.byValTSt, SizeConst =256)] public readonly Byte[] TrackData;

I have used this successfully on char arrays in the past.

weismat
Shouldn't that be UnmanagedType.ByValArray?
OregonGhost
Since this is a byte array and not a string, it should probably be [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]However this did not make any difference, and I still get the same error.
Johnny Egeland
OregonGhost is right - not sure if it makes a difference though - can not test it here...
weismat
Check your Calling convention - maybe this does not fit as well.
weismat
+1  A: 

Looks to me like the problem is that you're passing a reference by reference. Since MSTrackData is a class (i.e. reference type), passing it by reference is like passing a pointer-to-pointer.

Change your managed prototype to:

public static extern bool EncodeMagstripe(IntPtr hDC,
                    MSTrackData pTrack1,
                    MSTrackData pTrack2,
                    MSTrackData pTrack3,
                    MSTrackData reserved);

See the MSDN article about passing structures.

Jim Mischel
Yes, nice how most Marshalling is automatic. But Johnny also needs to pass NULL for 1 of the MsTrackData's.
Henk Holterman
And since MSTrackData is a reference type in his example, it's trivial to pass null.
Jim Mischel
+1  A: 

All the answers given so far have a bit of the answer but are incomplete. You need the MarshalAs - ByValArray as well as the new, your MSTrackDatas are already references so you do not need to pass them by ref and you must check what calling convention ICEAPI represents, if it is StdCall you don't need to change anything but if it is cdecl you will need to add the CallingConvention to your DllImport attribute. Also, you may need to add a MarshalAs attribute to your bool return value to make sure it is marshaled as 4 byte WinApi style bool. Here are the declares you'll (probably) need:

public const int MAX_ICE_MS_TRACK_LENGTH = 256;

[StructLayout(LayoutKind.Sequential)]
public class MSTrackData {
    public UInt32 nLength;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
    public Byte[] TrackData = new byte[MAX_ICE_MS_TRACK_LENGTH];
}

[DllImport("ICE_API.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EncodeMagstripe(IntPtr hDC,
                [In] MSTrackData pTrack1,
                [In] MSTrackData pTrack2,
                [In] MSTrackData pTrack3,
                [In] MSTrackData reserved);
Stephen Martin
The ICEAPI refers to WINAPI, so I also set "CallingConvention = CallingConvention.Winapi" in the DllImport attribute. After implementing your suggestion, the call worked perfectly :-)Thank you very much :-)
Johnny Egeland
A: 

I had almost exactly the same problem - but with ReadMagstripe. And the solution provided here for EncodeMagstripe did not work for ReadMagstripe! I think the reason it did not work was that ReadMagstripe has to return data into the TRACKDATA structure/class, while EncodeMagstripe only passes data to the dll and data in TRACKDATA does not need to be changed. Here is the implementation that eventually worked for me - both with EncodeMagstripe and ReadMagstripe:

    public const int MAX_ICE_MS_TRACK_LENGTH = 256;
    [StructLayout(LayoutKind.Sequential)]
    public struct TRACKDATA
    {  
        public UInt32 nLength;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
        public string szTrackData;
    }


    [DllImport("ICE_API.dll", EntryPoint="_ReadMagstripe@20", CharSet=CharSet.Auto, 
        CallingConvention=CallingConvention.Winapi, SetLastError=true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool ReadMagstripe(int hdc, ref TRACKDATA ptrack1, ref TRACKDATA ptrack2,
         ref TRACKDATA ptrack3, ref TRACKDATA reserved);

    [DllImport("ICE_API.dll", EntryPoint="_EncodeMagstripe@20", CharSet=CharSet.Auto,
        CallingConvention = CallingConvention.Winapi, SetLastError=true)]
    public static extern bool EncodeMagstripe(int hdc, [In] ref TRACKDATA ptrack1, [In] ref TRACKDATA ptrack2,
        [In] ref TRACKDATA ptrack3, [In] ref TRACKDATA reserved);


/*
  ....
*/


 private void EncodeMagstripe()
    {
        ICE_API.TRACKDATA track1Data = new ICE_API.TRACKDATA();
        ICE_API.TRACKDATA track2Data = new ICE_API.TRACKDATA();
        ICE_API.TRACKDATA track3Data = new ICE_API.TRACKDATA();
        ICE_API.TRACKDATA reserved = new ICE_API.TRACKDATA();

        //if read magstripe
        bool bRes = ICE_API.ReadMagstripe(printer.Hdc, ref track1Data, ref track2Data,
            ref track3Data, ref reserved);

        //encode magstripe
        if (bRes)
        {
            track2Data.szTrackData = "1234567890";
            track2Data.nLength = 10;

            bRes = ICE_API.EncodeMagstripe(printer.Hdc, ref track1Data, ref track2Data, ref track3Data, ref reserved);
        }
    }
Evgeny
My above solution would work fine for ReadMagstripe but since this function returns data in the MSTrackData structures the [In] attribute on each of the MSTrackData parameters would need to be changed to [In, Out].
Stephen Martin
Oh I see ... I did not know exactly what the purpose of the In attrubutes is.
Evgeny