P/Invoke 机制
C#调用采用C/C++编写的dll库,托管代码中对非托管接口进行定义 + 数据封送
C#调用C++过程
1.查询包含调用函数的DLL
2.将DLL加载到内存中(首次加载,并缓存)
3.查找函数在内存中的地址,并将其参数压入堆栈,封送所需数据。
4.调用C++对应函数。
注:在我们使用第三方DLL的时候,可以使用 dumpbin.exe工具,查看DLL里面的接口。
定义
声明信息中包含指定的dll和dll内指定的函数,此方法是全局C函数。
extern:告诉编译器,我属于外部接口
static:非成员方法的好处是,不需要构造对象
DllImport:声明依赖的dll,它有很多参数,举几个常见的。
(1)EntryPoint:如果用它指定C++函数名,C#里面声明的函数就可以不与C++一致。
(2)CallingConvention:约定传递的参数的堆栈,由谁来清理,是C#端还是C++端。默认是由系统环境决定。
(3)CharSet:两个作用。1.确定字符串参数的封送格式。2.确定调用C++里面那一类接口,有些接口有不同字符编码格式的版本。
内存释放
C#调用C++时,有时候涉及指针和堆内存对象,如何释放比较合理?在C++里面写一个释放的方法,供C#调用。
数据封送
由封送拆收器完成,将数据从非托管转换为托管类型,然后复制内存到托管内存,调用完后释放掉封送的内存。
封送字符串
string:string类型,参数不可变,如果作为方法参数,只能in不能out
StringBuilder:默认带in/out属性,所以它既可以字符串到C++,也可以从C++获取字符串。最优实践:定义StringBuilder(size)的时候初始化一个确定的size,其次是采用Unicode编码,可以减少一次拷贝。
CharSet & MarshalAs:CharSet 可以定义整个函数所有参数的字符编码,MarshalAs可以指定单独某一个。
封送结构体
定义结构体:
1.数据结构要一致,定义顺序一致,类型要一致,占用内存要一致
2.ref:代表参数不是值类型,而是引用类型,具备out特性。修改后,C#端内容会改变。
3.StructLayout:CharSet指定字符串类型,Pack = 8指定在内存中的对齐方式,Size占用内存大小。
4.LayoutKind:Sequential字段按定义的顺序在内存中布局,Explicit精确控制位置,Auto由系统自动布局。默认是Sequential用这个就好,规范做事不容易出错。
封送指针
对于非托管内存数据(C++),如果要返回到托管内存(C#),最万能的做法就是IntPtr + Marshal组合,一力降十会。
最后:
1.P/Invoke机制虽然保证了C#对Native的访问,但是毕竟是夸语言沟通,参数传递时涉及拷贝,成本并不低。
2.关于参数的传递,应该力求简洁,如果你的结构体嵌套的很复杂,或许应该考虑换种方式组织代码。