使用平台调用(PInvoke)实现C#调用非托管C++代码
Posted kasperskynod
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用平台调用(PInvoke)实现C#调用非托管C++代码相关的知识,希望对你有一定的参考价值。
1.问题描述
众所周知,不同的语言有不同的优势,如何让不同的语言在一个程序中“各司其职”、“分工协作”一直是一个人们想要达到的目标。有许多时候,我们需要用C#语言调用C++语言写成的代码:一方面,C#在编写GUI时更加方便(因为有WPF),而C++的执行效率之高是实现许多复杂算法的首选语言;另一方面,许多既有代码是通过C++语言编写的(各种库,甚至Win32API),我们也有在C#中重用既有C++代码的需求。本文讨论如何在C#中调用C++代码。
C#代码是基于.NET的托管代码,而C++代码是非托管代码(native code)。微软的.NET提供两种机制实现了托管代码(不只是C#,还包括VB.NET等语言代码)与非托管C++代码的交互:
(1) C#通过PInvoke技术直接调用由C++编写的DLL的导出函数;
(2) 通过COM技术
本文主要讨论方法1中PInvoke技术的应用。
2.使用C++编写动态链接库(DLL)
C#调用C++代码的第一步是把C++代码写成动态链接库(DLL)的形式。动态链接库的概念详见:
动态链接库(编程相关名词)_百度百科
http://baike.baidu.com/link?url=fsEmhVS4wgOfZgZM–kg2OcfJ2MkiLeetn5_VzEDk1bseKFC_72G_C6zkkeUT0HoOpGehw_wwizvsfVGB8Ktbp0UkyjvfNzqrhlBNlQ1XIS
微软官方文档Dynamic-Link Libraries (Windows)
https://msdn.microsoft.com/en-us/library/ms682589(v=vs.85).aspx
2.1在VS中新建DLL工程
编写DLL的第一步是在VS中新建一个DLL工程。选择文件-新建工程,项目类型选择Win32 Console Application。
在Application Type中选择DLL
点击Finish,一个DLL工程就新建好了
2.2编写导出函数
通过PInvoke技术只能调用DLL中的导出函数。所谓导出函数,就是DLL供外界调用的函数(换句话说,不是DLL中所有的函数都能被外界调用,DLL中也包含许多“私有函数”)。
导出函数的声明如下所示:
extern "C" __declspec(dllexport) void FunctionName(Parameter List);
在这个声明中,extern说明这个函数是一个外部函数;”C”说明此导出函数是C风格的函数,按照C语言的方式进行编译和链接;__declspec(dllexport)说明这是一个DLL导出函数;后面是函数名和参数列表。
把导出函数的声明写在相应的头文件中,并把此函数的实现写在源文件中,然后build整个项目,就能得到我们想要的DLL文件了。此处为了示例,写一个简单的示例函数:
//此函数为示例导出函数,它接受一个int型指针,并把指针指向的数+1
//头文件
extern "C" __declspec(dllexport) void testFunc(int *para);
//源文件
void testFunc(int *para)
(*para)++;
build此工程,得到ConsoleApplication1.dll。此处介绍一个好用的软件DLL Export Viewer,能查看DLL中的导出函数。使用Dll Export Viewer查看上面的示例dll如下图所示:
通过这款软件可以清楚的看到DLL的导出函数名称及地址等信息。至此,我们的DLL文件已经编写完了。
3.在C#中使用PInvoke技术调用DLL中的导出函数
3.1 DLL函数的封装
我们要在C#调用DLL中的导出函数,首先要用C#写一个方法声明,使此方法与DLL中某个导出函数“绑定”。以后当程序的其余部分调用此函数时,就会执行与此函数“绑定”的DLL中的导出函数的代码,从而实现了C#对C++的调用。这个“绑定”的过程是通过特性实现的。示例的方法声明如下:
//编写一个C#方法,使其“绑定”到DLL中的导出函数
[DllImport("ConsoleApplication1.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void testFunc(IntPtr para);
我们观察这个方法声明,首先它是由static extern 修饰的,表明他是一个静态外部方法,所有的封装方法都要声明成static extern的;其次,这个方法以分号结束,并没有提供函数体,说明这只是一个方法声明,它的实现在DLL的绑定导出函数中;最后,此方法用[DllImport]特性进行修饰,[DllImport]特性实现了此C#方法与DLL中导出函数的“绑定”,DllImport的属性说明如下表:
如果封装方法与导出函数同名,可以不用谢EntryPoint属性,否则应该在EntryPoint属性中指定入口函数名。
另外要特别注意,C#应用程序的位数和DLL的位数必须匹配,也就是说,要么程序和DLL都是x86平台的,要么都是x64平台的,平台不一致不能调用!
3.2封装函数的调用
把DLL的导出函数用C#进行封装后,就可以在程序的其他位置调用此函数了,与调用一般的方法没有区别。这里略。
3.3向DLL导出函数传递参数
这一节尤其重要,是整个调用的关键所在。调用导出函数,不可避免的要向导出函数传递参数。但是,封装方法是C#方法,采用C#的类型;DLL导出函数使用的是C++类型。当在他们之间传递参数时,怎么把C#的类型和C++的类型进行转换呢?
.NET在托管代码调用非托管代码时,定义了一个类型转换规则。即一个CLR基本类型对应一个C++基本类型。而每个CLR基本类型又对应一个C#类型,因此C#的类型也与C++的类型存在一种映射关系,即:
C#基本类型 ↔ CLR基本类型 ↔ C++基本类型
因此,要在封装函数与导出函数之间传递参数,两者的数据类型必须相容。一般的,C#的int类型对应C++的int类型,其余的类型也有类似的对应关系。(这里本来要引用MSDN文档中的类型相容规则表,但是微软MSDN的链接挂了,如果日后找到了类型相容规则表,再补充上~)
除了基本数据类型外,封装函数与导出函数还能够传递指针。传递指针的方法非常简单,只需在 C# 中将方法参数定义为 ref 或 out,数据则通过指针而不是通过值传递。
C#中还有一种特殊的指针—-不透明指针System.IntPtr。当您使用 IntPtr 类型时,通常不使用 out 或 ref 参数,因为 IntPtr 意为直接持有指针。利用不透明指针,我们可以实现在封装函数与导出函数之间传递数组。代码如下:
//C#封装函数声明
private static extern void testFunc(IntPtr para);
//从一个数组获取IntPtr
public void AnotherMethod()
...
int[] array=new int[128];
GCHandle dllfind = GCHandle.Alloc(array, GCHandleType.Pinned);
testFunc(dllfind.AddrOfPinnedObject());
dllfind.Free();
...
通过上面这种方式,就实现了C#向非托管C++代码传递数组。
4.参考
1.微软MSDN文档–在 C# 中通过 P/Invoke 调用Win32 DLL
https://msdn.microsoft.com/zh-cn/library/aa686045.aspx
2.《C#高级编程(中文版第七版)》P750-P754:平台调用
以上是关于使用平台调用(PInvoke)实现C#调用非托管C++代码的主要内容,如果未能解决你的问题,请参考以下文章
C# DllImport“调用导致堆栈不对称。原因可能是托管的 PInvoke 签名与非托管的目标签名不匹配。请检查 PInvoke 签名的调用约定和参数与非托管的目标签名是否匹配 ”