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

寵の児 提交于 2019-12-05 02:09:56

[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?

A correct incantation is

string UnpackFixed(byte[] data, System.Text.Encoding encoding)
{
    int i;
    for (i = 0; i < data.Length; ++i)
        if(data[i] == (byte)0)
            break;
    return encoding.GetString(data, i);
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct DeviceInfo
{
    uint32 maxScanrate;
    uint32 minScanrate;
    uint32 maxNumOfPoints;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
    byte type[];
};

DeviceInfo pDevInfo = new DeviceInfo();
pDevInfo.type = new byte[32];
int r4 = GetDeviceInfo(0, ref pDevInfo);
Console.WriteLine("  - type: " + UnpackFixed(pDevInfo.type));

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 using System.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 where ASCII 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).

I think you got a problem with public string type in DeviceInfo. If you needed to pass a string to the native part, that would be fine, but I understand that you're getting a char* from (allocated by) the native part, and in that case you're losing the address of type that is managed (and that cannot be known).

The right way to do this would be to have:

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct DeviceInfo {
    public UInt32 maxScanrate;
    public UInt32 minScanrate;
    public UInt32 maxNumOfPoints;
    public IntPtr type; // HERE: char*
}

and then process type in the managed part, for instance like this:

unsafe void GetType(IntPtr strPtr) => return new string((char*)strPtr);

If the native part doesn't do the allocation you'll need to use Marshal.AllocHGlobal.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!