C#调用C++DLL注意事项:
1>C#值类型与引用类型的内存特点
2>平台调用中DllImport,StructLayout,MarshalAS的各属性及其含义
3>C++中结构体的内存布局规则
4>C#调用非托管代码时,各种参数的送封特点(主要是结构体,数组,字符串)
5>使用Marshal类的静态方法实现托管内存与非托管内存之间的转换
6>内存释放问题,即C#中如何释放非托管代码申请的内存
1>C#值类型与引用类型的内存特点
C#值类型的对象是在堆栈上分配的,不受垃圾回收器的影响。
C#引用类型的对象是在托管堆上分配的,对应的引用地址会存储在线程的堆栈区。
值类型包括C#的基本类型(用关键字int、char、float等来声明),结构(用struct关键字声明的类型),枚举(用enum关键字声明的类型);引用类型包括类(用class关键字声明的类型),委托(用delegate关键字声明的特殊类)和数组。
2>平台调用中DllImport,StructLayout,MarshalAS的各字段及其含义
DllImport属性:
dllName:
必须字段,指定对应的dll的路径,可以使用绝对路径也可以使用相对路径。如果使用相对路径,则系统会在以下三个文件夹下寻找该相对路径:1,exe所在文件夹;2,系统文件夹;3,Path环境变量指定的文件夹。
EntryPoint:
指定要调用的DLL入口点。注意如果使用extern"C" + __stdcall 则编译器会对函数名进行重整。最终的函数名会是_FuncName@8,其中8为FuncName函数所有参数的字节数。如果是extern "C"+__cdecl调用约定,则函数名不变。
举例:函数声明如下:
对应的函数入口点如下:
注:
其中extern"C" 使得C++编译器生成的函数名对于C编译器是能够理解的,因为C++编译器为了处理函数重载的情况,将参数类型加入到了函数的签名中,所以生成的函数入口只能C++编译器自己懂。而加入extern "C"则使得生成的函数签名能够被其他编译器理解。
__stdcall和__cdecl是两种调用约定方式。主要区别在于压入堆栈中的函数参数的清理方式,__stdcall规定被调用函数负责参数出栈,称自动清除;__cdecl则规定调用函数方负责参数出栈,称手动清除。编译器一般默认使用__cdel方式。
CharSet:
控制函数中与字符串有关的参数或 结构体参数中与字符串有关的参数 在封送时的编码方式。 编码方式有两种:ANSI和UNICODE。ANSI使用1个字节对前256个字符进行编码,而UNICODE使用两个字节对所有字符进行编码。.Net平台中使用的是Unicode格式。在C++中可以使用多种字符集。
从托管代码中传递字符串到非托管代码中时,如果非托管代码使用的是ANSI,则需指定封送方式为Charset.Ansi,封送拆收器会根据该设置将Unicode字符转换为ANSI字符,再复制到非托管内存中,如果非托管代码使用Unicode,则需要指定Charset.Unicode,封送拆收器则直接复制过去,在效率上会好一些。
MarshalAS属性:
用来指定单个参数或者是结构体中单个字段的封送方式。该属性有以下字段:
UnmanagedType:
必须字段。用于指定该参数对应非托管数据类型。由于C#中的数据类型和C++中的数据类型不是一一对应的,有些时候C#中的同一种数据类型可以对应于C++中的几种数据类型,所以需要指定该参数,封送拆收器会在两个类型之间进行相应的类型转换。
比如C#中的String类型,则可以对应于非托管C++中的char * 或者 wchat_t*。如果是char*,则指定UnmanagedType.LPStr,如果是wchat_t*,则指定为UnmanagedType.LPWStr。
另一个例子是C#中的托管类型System.Boolean可以对应非托管C++中的bool,但是C++中的bool可能是1个字节,2个字节或者4个字节。这时就需要指定为UnmanagedType.U1,UnmanagedType.U2或者UnmanagedType.U4。
本项目中KXTV_BOOLEAN为1字节无符号数:
typedef unsigned char KXTV_BOOLEAN;
所以在C#中:
using KXTV_BOOLEAN =System.Boolean;
[MarshalAs(UnmanagedType.U1)]
KXTV_BOOLEAN NetUserFlag,
count:
对于需要传递定长字符串或者数组的情况,需要使用count字段来指定字符串长度或者数组中元素的个数。
StructLayout属性:
控制C#中的结构体在内存中的布局。为了能够和非托管C++中的结构体在内存中进行转换,封送拆收器必须知道结构体中每一个字段在结构体中内存中的偏移量。
LayoutKind:
指定结构体的布局类型。有两种布局类型可以设置,1,Sequential:顺序布局。2,Explicit:精确布局。可以精确控制结构体中每个字段在非托管内存中的精确位置。
一般使用顺序布局方式,这也是C#默认的布局方式。
但是在以下两种情况下需要使用精确控制Explicit方式:
1,部分定义结构体中的字段。有些结构体很庞大,而C#中仅使用其中几个字段,则可以只定义那几个字段,但是要精确指定它们在非托管内存中精确偏移。该偏移应与有其他字段时的偏移一致。
2,非托管代码中的联合体,需要使用Explicit将字段重合在一起。
如:
[StructLayout(LayoutKind.Explicit,Pack=1)]
public struct KXTV_VALUE
{
[Fie