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:
Debug > Windows > Exceptions Settings > "Win32 Exceptions" checkbox ticked
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):
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!
A correct incantation is
I'm certain there is a way to do this with
string
but all the old obvious ways of doing it tended to pass the string to native code and get nothing back. Here, the exercise is to get a fixed-length byte string back. If you do solve it for string you will end up usingSystem.Text.Encoding.Default
which may or may not be right and there's no way to override it.System.Text.Encoding.ASCII
is plausibly wrong, in which case you need to deal with encodings.System.Text.Encoding.Default
might work whereASCII
didn't, in which case you should consider if you have weird failure modes on multi-byte character encodings. It's not clear if the device always uses the same encoding as the OS or if it assumes a fixed encoding (in which case you should specify the encoding).[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
declares that the field is stored achar[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.:This works for me with VS2017 C++ and .Net 4.6.1.
What happens if you change the C# declaration to this:
[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?I think you got a problem with
public string type
inDeviceInfo
. If you needed to pass astring
to the native part, that would be fine, but I understand that you're getting achar*
from (allocated by) the native part, and in that case you're losing the address oftype
that is managed (and that cannot be known).The right way to do this would be to have:
and then process
type
in the managed part, for instance like this:If the native part doesn't do the allocation you'll need to use Marshal.AllocHGlobal.