Search code examples
c#c++structpinvokeunions

C# pinvoke structs with union and arrays


im failing to define the correct C# code to work with a C++ library which defines a complex struct with union and arrays, i keep getting some memory exception while executing the C++ code, and im pretty sure its due to this.

The c++ struct is as follows, ignoring enums and other struct definition (i will post them if needed but they are pretty extense)

typedef struct
{
DG_CCTALK_APP_EVT_CODE  eEventCode;
DG_CCTALK_APP_EVT_TYPE  eEventType;
int         iTime;
int         iHandle;
unsigned char       uchAddress;
DG_CCTALK_BILL_INFO sBillInfo;
int         iBillAmount;
union
{
    long        lData;
    int     iData;
    unsigned char   ucArray[128];
    char        *cString;
    void        *pvoid;
} uData;
void            *_private;  // for use by cctalk app layer
} DG_CCTALK_APP_EVT, *PDG_CCTALK_APP_EVT;

And the C# code is this:

[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Explicit)]
    public struct Anonymous_92d21a81_2a8b_42f4_af32_f7606aa9cf40
    {
        //deberian llevar todos 0, pero el array genera problemas al ponerle 0, y cambiandolo explota en otro lado...

        /// int
        [System.Runtime.InteropServices.FieldOffsetAttribute(0)]
        public int lData;

        /// int
        [System.Runtime.InteropServices.FieldOffsetAttribute(0)]
        public int iData;

        /// unsigned char[128]
        [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 128, ArraySubType = System.Runtime.InteropServices.UnmanagedType.I1)]
        [System.Runtime.InteropServices.FieldOffsetAttribute(4)]
        public byte[] ucArray;

        /// char*
        [System.Runtime.InteropServices.FieldOffsetAttribute(0)]
        public System.IntPtr cString;

        /// void*
        [System.Runtime.InteropServices.FieldOffsetAttribute(0)]
        public System.IntPtr pvoid;
    }

    [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
    public struct DG_CCTALK_APP_EVT
    {

        /// DG_CCTALK_APP_EVT_CODE->_DG_CCTALK_APP_EVT_CODE
        public DG_CCTALK_APP_EVT_CODE eEventCode;

        /// DG_CCTALK_APP_EVT_TYPE->_DG_CCTALK_APP_EVT_TYPE
        public DG_CCTALK_APP_EVT_TYPE eEventType;

        /// int
        public int iTime;

        /// int
        public int iHandle;

        /// unsigned char
        public byte uchAddress;

        /// DG_CCTALK_BILL_INFO->_DG_CCTALK_BILL_INFO
        public DG_CCTALK_BILL_INFO sBillInfo;

        /// int
        public int iBillAmount;

        /// Anonymous_92d21a81_2a8b_42f4_af32_f7606aa9cf40
        public Anonymous_92d21a81_2a8b_42f4_af32_f7606aa9cf40 uData;

        /// void*
        public System.IntPtr _private;
    }

Delegates and a function import

public delegate void DG_CCTALK_APP_EVT_HANDLER(ref DG_CCTALK_APP_EVT pEVT);

[System.Runtime.InteropServices.DllImportAttribute("cctalk.dll", EntryPoint = "cctalk_app_enable_device", CallingConvention = CallingConvention.Cdecl)]
public static extern int cctalk_app_enable_device(ref DG_CCTALK_APP_EVT param0);

UPDATE: new code after an asnwer, still not working: The error is: "Attempt to read or write Protected Memory This is often an indicating that other memory is corrupt"

What is weird, is that i can use the struct on the first event call, i print all fields and they seem to be right (lData and iData have the same values, cData has a string I sent) but after the event ends the program dies. Seems like my C# code is breaking the struct or something...

[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
    public struct DG_CCTALK_APP_EVT
    {

        /// DG_CCTALK_APP_EVT_CODE->_DG_CCTALK_APP_EVT_CODE
        public DG_CCTALK_APP_EVT_CODE eEventCode;

        /// DG_CCTALK_APP_EVT_TYPE->_DG_CCTALK_APP_EVT_TYPE
        public DG_CCTALK_APP_EVT_TYPE eEventType;

        /// int
        public int iTime;

        /// int
        public int iHandle;

        /// unsigned char
        public byte uchAddress;

        /// DG_CCTALK_BILL_INFO->_DG_CCTALK_BILL_INFO
        public DG_CCTALK_BILL_INFO sBillInfo;

        /// int            
        public int iBillAmount;            

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)]
        public byte[] uData;

        #region setters y getters para mapeo

        public int lData
        {
            set
            {
                Array.Copy(BitConverter.GetBytes(value), uData, sizeof(int));
            }
            get
            {
                return BitConverter.ToInt32(uData, 0);
            }
        }

        public int iData
        {
            set
            {
                Array.Copy(BitConverter.GetBytes(value), uData, sizeof(int));
            }
            get
            {
                return BitConverter.ToInt32(uData, 0);
            }
        }

        public byte[] ucArray
        {
            set
            {
                Array.Copy(value, uData, 128);
            }
            get
            {
                return uData;

            }
        }

        public IntPtr cString
        {
            set
            {
                Array.Copy(BitConverter.GetBytes(value.ToInt32()), uData, 32);
            }
            get
            {
                return (IntPtr)BitConverter.ToInt32(uData, 0);
            }
        }

        public IntPtr pvoid
        {
            set
            {
                Array.Copy(BitConverter.GetBytes(value.ToInt32()), uData, 32);
            }
            get
            {
                return (IntPtr)BitConverter.ToInt32(uData, 0);
            }
        }


        #endregion

        /// void*
        public System.IntPtr _private;
    }

C++ declarations:

typedef void (*DG_CCTALK_APP_EVT_HANDLER)(PDG_CCTALK_APP_EVT pEVT);

DGCCTALK_PREFIX DG_ERROR cctalk_app_init(DG_CCTALK_APP_EVT_HANDLER pfnHandler);
DGCCTALK_PREFIX DG_ERROR cctalk_app_add_link(char *szPortName);

Note dad DGCCTALK_PREFIX is defined as __declspec(dllexport)

C# code for those:

public delegate void DG_CCTALK_APP_EVT_HANDLER(ref DG_CCTALK_APP_EVT pEVT);

[System.Runtime.InteropServices.DllImportAttribute("cctalk.dll", EntryPoint =     "cctalk_app_init", CallingConvention = CallingConvention.Cdecl)]
 public static extern int cctalk_app_init(DG_CCTALK_APP_EVT_HANDLER pfnHandler);

[System.Runtime.InteropServices.DllImportAttribute("cctalk.dll", EntryPoint = "cctalk_app_add_link", CallingConvention = CallingConvention.Cdecl)]
public static extern int cctalk_app_add_link(System.IntPtr szPortName);

C# code invoking them

var handler = new DG_CCTALK_APP_EVT_HANDLER(this._handler);
var resultado = cctalk_app_init(handler);
cctalk_app_add_link(Marshal.StringToHGlobalAnsi("port=" + port));

C# handler header

private void _handler(ref DG_CCTALK_APP_EVT pEVT)

Solution

  • As you have discovered, the marshaler does not like you trying to overlay the array with other fields in your translation of the union. You've suppressed the problem by changing the offset for the array to be 4 rather than 0. But that doesn't help.

    Now, since the marshaler won't deal with the union for you, you'll have to do it manually. I would handle this by omitting the non-array members of the union.

    [StructLayout(LayoutKind.Sequential)]
    public struct DG_CCTALK_APP_EVT
    {
        public DG_CCTALK_APP_EVT_CODE eEventCode;
        public DG_CCTALK_APP_EVT_TYPE eEventType;
        public int iTime;
        public int iHandle;
        public byte uchAddress;
        public DG_CCTALK_BILL_INFO sBillInfo;
        public int iBillAmount;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)]
        public byte[] uData;
        public IntPtr _private;
    }
    

    Note that I have removed much of the verbosity of your declaration. I suspect that the type was automatically generated by a tool. But all that verbosity makes it very hard to read.

    This will produce the correct layout for the struct, but will leave you with work to do to access the other fields in the union.

    So, how can we read those fields? Well, the data is contained in the byte array uData, so it is just a matter of reading the values from there. For instance, you could add the following property to the DG_CCTALK_APP_EVT struct:

    public int lData
    {
        get { return BitConverter.ToInt32(uData, 0); }
        set { uData = BitConverter.GetBytes(value); }
    }
    
    public int iData
    {
        get { return BitConverter.ToInt32(uData, 0); }
        set { uData = BitConverter.GetBytes(value); }
    }
    
    // etc.
    

    Note that the setter will obliterate all the but first 4 bytes of the array, since it overwrites uData. I don't know enough about the protocol of the native code to be sure that this is what you want. If it's not what you want, then you might write the setter like this:

    Array.Copy(BitConverter.GetBytes(value), uData, sizeof(int));
    

    You've now added the C++ side of the interop boundary. The most obvious problem is that your C# delegate appears to have the wrong calling convention. It uses stdcall but the C++ code expects cdecl. That's certainly enough to explain a catastrophic crash after your callback returns. You'll need to make sure that your delegate specifies the calling convention.

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void DG_CCTALK_APP_EVT_HANDLER(ref DG_CCTALK_APP_EVT pEVT);
    

    Your translation of cctalk_app_add_link is wrong too. It should be:

    [DllImport("cctalk.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int cctalk_app_add_link(string szPortName);
    

    Done like this you can simply pass a string and thus avoid the memory leak that your current implementation has.