Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to call a C++ function with a struct pointer parameter from C#?

Okay one more function that it's not yet working. I am basically calling some C++ functions from C# by using P/Invoke. The problematic function does query a show laser device for some device related information, such as minimal and maximal scan rates and maximal points per second.

The problematic function is:

int GetDeviceInfo(DWORD deviceIndex, DeviceInfo* pDeviceInfo);

Here's the C++ header file that I was given. That's a link to the very brief C++ SDK description. I don't have the sources to rebuild the DLL file and I also don't have the *.pdb file (the manufacturer can not supply it):

#pragma once

#ifdef STCL_DEVICES_DLL
#define STCL_DEVICES_EXPORT extern "C" _declspec(dllexport) 
#else
#define STCL_DEVICES_EXPORT extern "C" _declspec(dllimport)
#endif

enum SD_ERR
{
    SD_ERR_OK = 0,
    SD_ERR_FAIL,
    SD_ERR_DLL_NOT_OPEN,
    SD_ERR_INVALID_DEVICE,  //device with such index doesn't exist
    SD_ERR_FRAME_NOT_SENT,
};

#pragma pack (1)
struct LaserPoint
{
    WORD x;
    WORD y;
    byte colors[6];
};

struct DeviceInfo
{
    DWORD maxScanrate;
    DWORD minScanrate;
    DWORD maxNumOfPoints;
    char type[32];
};

//////////////////////////////////////////////////////////////////////////
///Must be called when starting to use
//////////////////////////////////////////////////////////////////////////
STCL_DEVICES_EXPORT int OpenDll();

//////////////////////////////////////////////////////////////////////////
///All devices will be closed and all resources deleted
//////////////////////////////////////////////////////////////////////////
STCL_DEVICES_EXPORT void CloseDll();

//////////////////////////////////////////////////////////////////////////
///Search for .NET devices (Moncha.NET now)
///Must be called after OpenDll, but before CreateDeviceList!
///In pNumOfFoundDevs can return number of found devices (optional)
//////////////////////////////////////////////////////////////////////////
STCL_DEVICES_EXPORT int SearchForNETDevices(DWORD* pNumOfFoundDevs);

//////////////////////////////////////////////////////////////////////////
///Creates new list of devices - previous devices will be closed
///pDeviceCount returns device count
//////////////////////////////////////////////////////////////////////////
STCL_DEVICES_EXPORT int CreateDeviceList(DWORD* pDeviceCount);

//////////////////////////////////////////////////////////////////////////
///Returns unique device name
///deviceIndex is zero based device index
//////////////////////////////////////////////////////////////////////////
STCL_DEVICES_EXPORT int GetDeviceIdentifier(DWORD deviceIndex, WCHAR** ppDeviceName);

//////////////////////////////////////////////////////////////////////////
///Send frame to device, frame is in following format:
///WORD x
///WORD y
///byte colors[6]
///so it's 10B point (=> dataSize must be numOfPoints * 10)
///scanrate is in Points Per Second (pps)
//////////////////////////////////////////////////////////////////////////
STCL_DEVICES_EXPORT int SendFrame(DWORD deviceIndex, byte* pData, DWORD numOfPoints, DWORD scanrate);

//////////////////////////////////////////////////////////////////////////
///Returns true in pCanSend if device is ready to send next frame
//////////////////////////////////////////////////////////////////////////
STCL_DEVICES_EXPORT int CanSendNextFrame(DWORD deviceIndex, bool* pCanSend);

//////////////////////////////////////////////////////////////////////////
///Send DMX if device supports it - pDMX must be (!!!) 512B long
//////////////////////////////////////////////////////////////////////////
STCL_DEVICES_EXPORT int SendDMX(DWORD deviceIndex, byte* pDMX);

//////////////////////////////////////////////////////////////////////////
///Send blank point to position x, y
//////////////////////////////////////////////////////////////////////////
STCL_DEVICES_EXPORT int SendBlank(DWORD deviceIndex, WORD x, WORD y);

//////////////////////////////////////////////////////////////////////////
///Get device info
//////////////////////////////////////////////////////////////////////////
STCL_DEVICES_EXPORT int GetDeviceInfo(DWORD deviceIndex, DeviceInfo* pDeviceInfo);

This is the complete C# test code I am currently using. All the functions work fine, except for GetDeviceInfo(...):

using System;
using System.Threading;
using System.Runtime.InteropServices;

namespace MonchaTestSDK {

    public class Program {

        [DllImport("..\\..\\dll\\StclDevices.dll", CallingConvention = CallingConvention.Cdecl)]                                    // OK
        public static extern int OpenDll();
        [DllImport("..\\..\\dll\\StclDevices.dll", CallingConvention = CallingConvention.Cdecl)]                                    // OK
        public static extern void CloseDll();
        [DllImport("..\\..\\dll\\StclDevices.dll", CallingConvention = CallingConvention.Cdecl)]                                    // OK
        public static extern int SearchForNETDevices(ref UInt32 pNumOfFoundDevs);
        [DllImport("..\\..\\dll\\StclDevices.dll", CallingConvention = CallingConvention.Cdecl)]                                    // OK
        public static extern int CreateDeviceList(ref UInt32 pDeviceCount);
        [DllImport("..\\..\\dll\\StclDevices.dll", CallingConvention = CallingConvention.Cdecl)]                                    // OK
        public static extern int GetDeviceIdentifier(UInt32 deviceIndex, out IntPtr ppDeviceName);
        [DllImport("..\\..\\dll\\StclDevices.dll", CallingConvention = CallingConvention.Cdecl)]                                    // OK
        public static extern int SendFrame(UInt32 deviceIndex, LaserPoint[] pData, UInt32 numOfPoints, UInt32 scanrate);
        [DllImport("..\\..\\dll\\StclDevices.dll", CallingConvention = CallingConvention.Cdecl)]                                    // OK
        public static extern int CanSendNextFrame(UInt32 deviceIndex, ref bool pCanSend);
        [DllImport("..\\..\\dll\\StclDevices.dll", CallingConvention = CallingConvention.Cdecl)]                                    // OK
        public static extern int SendBlank(UInt32 deviceIndex, UInt16 x, UInt16 y);
        [DllImport("..\\..\\dll\\StclDevices.dll", CallingConvention = CallingConvention.Cdecl)]                                    // FAILS
        public static extern int GetDeviceInfo(UInt32 deviceIndex, ref DeviceInfo pDeviceInfo);

        [StructLayout(LayoutKind.Sequential, Pack=1)]
        public struct LaserPoint {
            public UInt16 x;
            public UInt16 y;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
            public byte[] colors;
        }

        [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
        public struct DeviceInfo {
            public UInt32 maxScanrate;
            public UInt32 minScanrate;
            public UInt32 maxNumOfPoints;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string deviceType;
        }

        public static void Main(string[] args) {
            Console.WriteLine("Moncha SDK\n");

            OpenDll();
            Console.WriteLine("StclDevices.dll is open.");

            UInt32 deviceCount1 = 0;
            int r1 = SearchForNETDevices(ref deviceCount1);
            Console.WriteLine("SearchForNETDevices() [" + r1+"]: "+deviceCount1);

            UInt32 deviceCount2 = 0;
            int r2 = CreateDeviceList(ref deviceCount2);
            Console.WriteLine("CreateDeviceList() ["+r2+"]: "+deviceCount2);

            IntPtr pString;
            int r3 = GetDeviceIdentifier(0, out pString);
            string devname = Marshal.PtrToStringUni(pString);
            Console.WriteLine("GetDeviceIdentifier() ["+r3+"]: "+devname);

            DeviceInfo pDevInfo = new DeviceInfo();
            pDevInfo.type = "";
            int r4 = GetDeviceInfo(0, ref pDevInfo);
            Console.WriteLine("GetDeviceInfo() ["+r4+"]: ");
            Console.WriteLine("  - min: "+pDevInfo.minScanrate);
            Console.WriteLine("  - max: " + pDevInfo.maxScanrate);
            Console.WriteLine("  - points: " + pDevInfo.maxNumOfPoints);
            Console.WriteLine("  - type: " + pDevInfo.deviceType);

            Thread.Sleep(5000);
            CloseDll();
        }

    }
}

On line 73 line 64 (cp. screenshot):

int r4 = GetDeviceInfo(0, ref pDevInfo); 

I receive the following error:

An unhandled exception of type 'System.NullReferenceException' occured in MonchaTestSDK.exe
Additional information: Object reference not set to an instance of an object

This is the stack trace (can't provide better stack trace without the DLL's *.pdb file I guess):

MonchaTestSDK.exe!MonchaTestSDK.Program.Main(string[] args) Line 73 + 0xa bytes C# mscoreei.dll!73a8d91b()
[Frames below may be incorrect and/or missing, no symbols loaded for mscoreei.dll]
mscoree.dll!73cae879()
mscoree.dll!73cb4df8()
kernel32.dll!74a08654()
ntdll.dll!77354b17()
ntdll.dll!77354ae7()

Some disassembly:

            int r4 = GetDeviceInfo(0, ref pDevInfo);
05210749  int         3  
0521074A  push        ebp  
0521074B  cwde  
0521074C  xor         ecx,ecx  
0521074E  call        0521011C  
05210753  int         3  
05210754  test        dword ptr [eax-1],edx  
05210757  ?? ?? 
05210758  dec         dword ptr [ebx-0AF7Bh]  
0521075E  dec         dword ptr [ecx-6F466BBBh]

Any idea what I am doing wrong here?


Update 1: Suggested debug options:

As suggested in the comments, I tried to enable native/unmanaged code debugging:

  1. Debug > Windows > Exceptions Settings > "Win32 Exceptions" checkbox ticked

  2. Project > Properties > Debug tab > "Enable unmanaged code debugging" checkbox ticked

I still don't get any meaningful exception stack. The manufacturer can't supply me the DLL's *.pdb file.

Here's an image showing the debugger when stopped at the problematic line (debug settings are also shown):

Here's an image showing the debugger when stopped at the problematic line (debug settings are also shown)


Update 2: Minimal Required Code (cp. comment of mpromonet)

This is the minimal required code to be able to call GetDeviceInfo(...):

public static void Main(string[] args) {
    OpenDll();
    UInt32 deviceCount = 0;
    CreateDeviceList(ref deviceCount);
    DeviceInfo pDevInfo = new DeviceInfo();
    GetDeviceInfo(0, ref pDevInfo);            // error occurs on this line
    CloseDll();
}

This leads to the exact same error as before:

An unhandled exception of type 'System.NullReferenceException' occured in MonchaTestSDK.exe
Additional information: Object reference not set to an instance of an object

Removing the call GetDeviceInfo(0, ref pDevInfo); from the code above allows the program to exit without any error.


Update 3: Removing char[] deviceType from DeviceInfo struct completely

I removed char[] deviceType from the struct defintion:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct DeviceInfo {
    public UInt32 maxScanrate;
    public UInt32 minScanrate;
    public UInt32 maxNumOfPoints;
    //[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    //public string deviceType;
}

When I run my C# test code now, I successfully receive maxScanrate, minScanrate and maxNumOfPoints back from the C++ DLL. Here's the corresponding console output:

GetDeviceInfo() [0]:
   - min: 1000
   - max: 40000
   - points: 3000

Finally ending in the following error message:

Exception thrown at 0x67623A68 (clr.dll) in MonchaTestSDK.exe: 0xC0000005: Access violation reading location 0x00000000.


Final Update

I finally got an updated DLL from the manufacturer. There was indeed a bug within the SDK that caused the stack to get corrupted. So basically the following solution now works fine without any issues:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct DeviceInfo {

    public UInt32 maxScanrate;
    public UInt32 minScanrate;
    public UInt32 maxNumOfPoints;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string deviceType;

}

private void queryDeviceProperties(UInt32 index) {
    HwDeviceInfo pDevInfo = new HwDeviceInfo();
    int code = GetDeviceInfo(index, ref pDevInfo);
    if(code==0) {
        Console.WriteLine(pDevInfo.minScanrate);
        Console.WriteLine(pDevInfo.maxScanrate);
        Console.WriteLine(pDevInfo.maxNumOfPoints);
        Console.WriteLine(pDevInfo.type);
    } else {
        Console.WriteLine("Error Code: "+code);
    }
}

Thank you all for the great support!

like image 551
salocinx Avatar asked May 02 '18 15:05

salocinx


People also ask

How do you call a function with a pointer argument?

To pass the value by pointer, argument pointers are passed to the functions just like any other value. So accordingly you need to declare the function parameters as pointer types as in the following function swap(), which exchanges the values of the two integer variables pointed to by its arguments.

Can you pass a struct as a parameter in C?

A structure can be transferred to a function either using call by value or call by reference scheme. Remember that C only implements call by value scheme of parameter transmission. Call by reference is indirectly implemented by passing address of variable.

How can we access a member of a pointer type structure variable?

To access members of a structure using pointers, we use the -> operator. In this example, the address of person1 is stored in the personPtr pointer using personPtr = &person1; . Now, you can access the members of person1 using the personPtr pointer.


1 Answers

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] declares that the field is stored a char[32] array as in the header, i.e. space for 31 characters and a null terminator.

Marshalling this to a string shouldn't be a problem, nothing that the dll writes to the array should be able to cause a NullReferenceException.

I can compile a stub dll that loads fine using your C# code and can send back ANSI strings, with addition of typedef byte... and a stub method body e.g.:

int GetDeviceInfo(DWORD deviceIndex, DeviceInfo* pDeviceInfo)
{
    std::string testString = "test string thats quite loooooooong"; 
    pDeviceInfo->maxScanrate = 1234;
    pDeviceInfo->minScanrate = 12345;
    pDeviceInfo->maxNumOfPoints = 100 + deviceIndex;
    sprintf_s(pDeviceInfo->type, "%.31s", testString.c_str());
    return 0;
}

This works for me with VS2017 C++ and .Net 4.6.1.

What happens if you change the C# declaration to this:

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct DeviceInfo
    {
        public UInt32 maxScanrate;
        public UInt32 minScanrate;
        public UInt32 maxNumOfPoints;
        public UInt64 deviceTypePart1;
        public UInt64 deviceTypePart2;
        public UInt64 deviceTypePart3;
        public UInt64 deviceTypePart4;

        public string GetDeviceType()
        {
            if (Marshal.SizeOf(this) != 44) throw new InvalidOperationException();
            List<byte> bytes = new List<byte>();
            bytes.AddRange(BitConverter.GetBytes(deviceTypePart1));
            bytes.AddRange(BitConverter.GetBytes(deviceTypePart2));
            bytes.AddRange(BitConverter.GetBytes(deviceTypePart3));
            bytes.AddRange(BitConverter.GetBytes(deviceTypePart4));
            return Encoding.GetEncoding(1252).GetString(bytes.ToArray());
        }
    }

[Edit]

I've no idea why hand cranking the marshaling fixes this - be sure to 'load test' in case there are heap/stack corruption bugs still lurking.

In your old code, does Marshal.SizeOf return something other than 44?

like image 61
Peter Wishart Avatar answered Oct 17 '22 05:10

Peter Wishart