我在C库中有一个结构:
#pragma pack(push, packing)
#pragma pack(1)
typedef struct
{
unsigned int ipAddress;
unsigned char aMacAddress[6];
unsigned int nodeId;
} tStructToMarshall;
__declspec(dllexport) int SetCommunicationParameters(tStructToMarshall parameters);
此代码使用cl /LD /Zi Communication.c
编译,以生成用于调试的DLL和PDB文件。
要从.Net应用程序使用此代码,我使用P/Invoke Interop Assistant为包装器DLL生成C#代码:
这导致显示的C#包装器,我修改它使用正确的DLL而不是"<unkown>"
。另外,我确实想要aMacAddress
的字节数组,而不是字符串(虽然我理解这通常会有用):
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
public struct tStructToMarshall
{
/// unsigned int
public uint ipAddress;
/// unsigned char[6]
[System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst = 6)]
public byte[] aMacAddress;
// ^^^^^^ Was "string"
/// unsigned int
public uint nodeId;
}
public partial class NativeMethods
{
internal const string DllName = "lib/Communication.dll";
/// Return Type: int
///parameters: tStructToMarshall->Anonymous_75c92899_b50d_4bea_a217_a69989a8d651
[System.Runtime.InteropServices.DllImportAttribute(DllName, EntryPoint = "SetCommunicationParameters")]
// ^^^^^^^ Was "<unknown>"
public static extern int SetCommunicationParameters(tStructToMarshall parameters);
}
我有两个问题: 1.当我将结构的值设置为非零值并查找节点ID时,它会被破坏或损坏。 IP地址和MAC地址都很好,但是数组之后的任何结构成员(包括其他数据类型)都被破坏,即使我指定了一位数值,也会在C输出中显示非常大的数字。 2.当我调用该方法时,我收到一条错误消息:
对PInvoke函数的调用''使堆栈失衡。这很可能是因为托管PInvoke签名与非托管目标签名不匹配。检查PInvoke签名的调用约定和参数是否与目标非托管签名匹配。
尝试调用不带参数的方法不会生成此异常。而且我很确定它与目标签名相匹配,因为这就是我生成它的方式!
如何解决这些问题?
答案 0 :(得分:2)
这种腐败&#39;是由对齐问题引起的。互操作助手忽略#pragma pack(1)
指令,并使用默认值described here。
使用以下规则对齐类型实例的字段:
类型的对齐方式是其最大元素(1,2,4,8等,字节)的大小或指定的包装大小,以较小者为准。
每个字段必须与其自身大小的字段(1,2,4,8等,字节)或类型的对齐方式对齐,以较小者为准。由于类型的默认对齐方式是其最大元素的大小(大于或等于所有其他字段长度),这通常意味着字段按其大小对齐。例如,即使一个类型中的最大字段是64位(8字节)整数或者Pack字段设置为8,字节字段在1字节边界上对齐,Int16字段在2字节边界上对齐,并且Int32字段在4字节边界上对齐。
- 在字段之间添加填充以满足对齐要求。
您在C中指定字段应在1字节边界上对齐。但是,你的C#代码假设那里没有填充,特别是在你的6字节结构之后:
使用IP地址0x01ABCDEF,MAC地址{0x01,0x02,0x03,0x04,0x05,0x06}和节点ID 0x00000001,内存看起来像这样(忽略了字符串问题,如果你得到它就不会有问题对齐权):
Byte Value C expects .NET Expects:
0 0x01 \ \
1 0xAB } IP Address } IP Address
2 0xCD | |
3 0xEF / /
4 0x01 } aMacAddress[0] } aMacAddress[0]
5 0x02 } aMacAddress[1] } aMacAddress[1]
6 0x03 } aMacAddress[2] } aMacAddress[2]
7 0x04 } aMacAddress[3] } aMacAddress[3]
8 0x05 } aMacAddress[4] } aMacAddress[4]
9 0x06 } aMacAddress[5] } aMacAddress[5]
10 0x00 \ } Padding
11 0x00 } Node ID } Padding
12 0x00 | \
13 0x01 / } Node ID
14 0x?? } Unititialized |
15 0x?? } Unititialized /
请注意,.NET期望节点ID(4字节值)从地址12开始,可以被4整除。它实际上使用了未初始化的内存,这会导致错误的结果。
将命名参数Pack=1
添加到您对StructLayoutAttribute的调用中:
[System.Runtime.InteropServices.StructLayoutAttribute(
System.Runtime.InteropServices.LayoutKind.Sequential, Pack=1, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
// ^^^^^^ - Here
这是由不同的calling conventions引起的。当您使用参数调用方法时,这些参数将进入堆栈。在某些调用约定下,调用者在方法返回后清理堆栈。在其他情况下,被调用函数会在返回之前清除。
使用cl
编译未注释的函数时,它使用cdecl
约定,该约定声明:
来电者清理堆栈。这样就可以使用varargs调用函数,这使得它适用于接受可变数量参数的方法,例如
printf
。
因此是C编译器的一个很好的默认值。将函数导入.NET时,它使用stdcall
约定,该约定声明:
被调用者清理堆栈。这是使用平台调用调用非托管函数的默认约定。
这在Windows API中使用(可能是P / Invoke最常用的库),因此是P / Invoke的一个很好的默认值,但两者不兼容。
several other questions中对此进行了一点描述(可能是因为它有一个与您的结构损坏不同的Googleable错误消息)并且已被回答here。
将CallingConvention = CallingConvention.Cdecl
添加到您的DllImportAttribute:
[System.Runtime.InteropServices.DllImportAttribute(DllName, EntryPoint = "SetCommunicationParameters", CallingConvention = CallingConvention.Cdecl)]