C#与非托管win32函数互操作方法
2013-02-19 16:48 埋头前进的码农 阅读(592) 评论(0) 编辑 收藏 举报一、引言
.NET平台下实现互操作性有三种技术——平台调用,C++ Interop和COM Interop,下面介绍第一种技术,即平台调用。然而朋友们应该会有这样的疑问,平台调用到底有什么用呢? 为什么我们要用平台调用的技术了?对于这两个问题的答案就是——平台调用可以帮助我们实现在.NET平台下(也就是指用C#、VB.net语言写的应用程序下)可以调用非托管函数(指定的是C/C++语言写的函数)。这样如果我们在.NET平台下实现的功能有现有的C/C++ 函数实现了这样的功能,这时候我们完全没必要自己再用托管语言(如C#、vb.net)去实现一个这样的功能,这时候我们应该想到 “拿来主义”,直接使用平台调用技术调用C/C++ 实现的函数。然而在实际应用中,使用平台调用技术来调用Win32 API较为普遍,所以在这个专题中将为大家具体介绍了如何使用平台调用来调用Win32函数以及调用过程中应该注意的问题,下面就从一个具体的实例开始本专题的介绍。
二、如何使用平台调用Win32 函数——从实例开始
使用.NET平台调用来调用非托管函数的步骤如下:
(1). 获得非托管函数的信息,即dll的名称,需要调用的非托管函数名等信息
(2). 在托管代码中对非托管函数进行声明,并且附加平台调用所需要属性
(3). 在托管代码中直接调用第二步中声明的托管函数
然而调用Win32 API函数还有一些问题需要注意的地方, 首先, 因为很多Win32 API函数都有ANSI和Unicode两个版本,所以在托管代码声明时需要指定调用调用函数的版本。 然而很多Win32 API函数有ANSI和Unicode两个版本并不是随便说说的,而是有根据的。大家从调用步骤中可以看出,第一步就需要知道非托管函数声明,为了找到需要需要调用的非托管函数,可以借助两个工具——Visual Studio自带的dumpbin.exe和depends.exe,dumpbin.exe 是一个命令行工具,可以用于查看从非托管DLL中导出的函数等信息,可以通过打开Visual Studio 2010 Command Prompt(中文版为Visual Studio 命令提示(2010)),然后切换到DLL所在的目录,输入 dummbin.exe/exports dllName, 如 dummbin.exe/exports User32.dll 来查看User32.dll中的函数声明,关于更多命令的参数可以参看MSDN; 然而 depends.exe是一个可视化界面工具,大家可以从 “VS安装目录\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Tools\Bin\” 这个路径找到,然后双击 depends.exe 就可以出来一个可视化界面(如果某些人安装的VS没有附带这个工具,也可以从官方网站下载:http://www.dependencywalker.com/),如下图:
上图中 我用红色标示出 MessageBox 有两个版本,而MessageBoxA 代表的就是ANSI版本,而MessageBoxW 代笔的就是Unicode版本,这也是上面所说的依据。下面就看看 MessageBox的C++声明的(更多的函数的定义大家可以从MSDN中找到,这里提供MessageBox的定义在MSDN中的链接:http://msdn.microsoft.com/en-us/library/windows/desktop/ms645505(v=vs.85).aspx ):
int WINAPI MessageBox( _In_opt_ HWND hWnd, _In_opt_ LPCTSTR lpText, _In_opt_ LPCTSTR lpCaption, _In_ UINT uType );
现在已经知道了需要调用的Win32 API 函数的定义声明,下面就依据平台调用的步骤,在.NET 中实现对该非托管函数的调用,下面就看看.NET中的代码的:
using System; // 使用平台调用技术进行互操作性之前,首先需要添加这个命名空间 using System.Runtime.InteropServices; namespace 平台调用Demo { class Program { // 在托管代码中对非托管函数进行声明,并且附加平台调用所需要属性 // 在默认情况下,CharSet为CharSet.Ansi // 指定调用哪个版本的方法有两种——通过DllImport属性的CharSet字段和通过EntryPoint字段指定 // 在托管函数中声明注意一定要加上 static 和extern 这两个关键字 [DllImport("user32.dll")] public static extern int MessageBox1(IntPtr hWnd, String text, String caption, uint type); // 在默认情况下,CharSet为CharSet.Ansi [DllImport("user32.dll")] public static extern int MessageBoxA(IntPtr hWnd, String text, String caption, uint type); // 在默认情况下,CharSet为CharSet.Ansi [DllImport("user32.dll")] public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type); // 第一种指定方式,通过CharSet字段指定 [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int MessageBox2(IntPtr hWnd, String text, String caption, uint type); // 通过EntryPoint字段指定 [DllImport("user32.dll", EntryPoint="MessageBoxA")] public static extern int MessageBox3(IntPtr hWnd, String text, String caption, uint type); [DllImport("user32.dll", EntryPoint = "MessageBoxW")] public static extern int MessageBox4(IntPtr hWnd, String text, String caption, uint type); static void Main(string[] args) { // 在托管代码中直接调用声明的托管函数 // 使用CharSet字段指定的方式,要求在托管代码中声明的函数名必须与非托管函数名一样 // 否则就会出现找不到入口点的运行时错误 //MessageBox1(new IntPtr(0), "Learning Hard", "欢迎", 0); // 下面的调用都可以运行正确 //MessageBoxA(new IntPtr(0), "Learning Hard", "欢迎", 0); //MessageBox(new IntPtr(0), "Learning Hard", "欢迎", 0); // 使用指定函数入口点的方式调用 //MessageBox3(new IntPtr(0), "Learning Hard", "欢迎", 0); // 调用Unicode版本的会出现乱码 MessageBox4(new IntPtr(0), "Learning Hard", "欢迎", 0); } } }
运行正确的结果为:
从代码的注释中可以看出,第一个调用MessageBox1会出现运行时错误,然而为什么改调用会出现 “无法在 DLL“user32.dll”中找到名为“MessageBox1”的入口点。”的错误呢? 为了知道为什么,这里就需要明白通过CharSet字段指定的这种方式的内部执行行为了。之所以会出现这个错误,是因为当指定CharSet为Ansi时,P/Invoke首先会通过根函数名在User32.dll中搜索,即不带后缀A的函数名MessageBox1 进行搜索,如果找到与跟函数一样名称的函数,就调用该函数;
如果没有找到则使用带后缀为A的函数MessageBox1A进行搜索,如果找到,则使用该函数,如果还是没有找到,则会出现 “无法在 DLL“user32.dll”中找到名为“MessageBox1”的入口点。”的错误。把CharSet指定为Unicode时,搜索方式是一样的,只是没找到根函数时会加W后缀进行搜索的。 从上面的搜索调用函数的过程中可以发现,因为user32.dll中既不存在MessageBox1函数也不存在MessageBox1A函数,所以才会出现调用错误。(朋友看到出现错误时,应该会有这样的疑问——我们如何捕捉错误来显示错误信息呢?这个疑问将会在下一部分解释。)
然而使用平台调用技术中,还需要注意下面4点:
(1). DllImport属性的ExactSpelling字段如果设置为true时,则在托管代码中声明的函数名必须与要调用的非托管函数名完全一致,因为从ExactSpelling字面意思可以看出为 "准确拼写"的意思,当ExactSpelling设置为true时,此时会改变平台调用的行为,此时平台调用只会根据根函数名进行搜索,而找不到的时候不会添加 A或者W来进行再搜索,. 例如,如果指定 MessageBox,则平台调用将搜索 MessageBox,如果它找不到完全相同的拼写则会出现找不到入口函数的错误。 从前面的代码中可以看出,我们在代码中并没有指定 ExactSpelling 字段,然而代码中却没有出现调用错误,这就说明在C#和托管C++语言中, ExactSpelling 默认值就是false的,然而在VB。NET中,ExactSpelling的默认值就是true, 所以以上代码如果转化为Vb.NET时,就需要显式指定ExactSpelling 字段为false,不然就会出现 “找不到函数入口的错误”。 为了让大家更加容易理解上面的理论,相信大家看到下面一张图会更加理解 ExactSpelling字段的含义的:
(2). 如果采用设置CharSet的值来控制调用函数的版本时,则需要在托管代码中声明的函数名必须与根函数名一致,否则也会调用出错,这点从平台调用过程中可以很好地理解,如果需要调用非托管函数名为 MessageBoxA,而你在托管代码声明为 MessageBox1,这样在搜索过程中明显就会提示找不到函数名的错误, 也就是上面代码中第一个调用出错的原因。
(3). 如果通过指定DllImport属性的EntryPoint字段的方式来调用函数版本时,此时必须相应地指定与之匹配的CharSet设置,意思就是——如果指定EntryPoint为 MessageBoxW,那么必须将CharSet指定为CharSet.Unicode,如果指定EntryPoint为 MessageBoxA,那么必须将CharSet指定为CharSet.Ansi或者不指定,因为 CharSet默认值就是Ansi。上面代码MessageBox4的调用之所以会出现乱码,是因为CharSet指定为Ansi(也是默认值)时, 平台调用将字符串按照ANSI编码方式封送到非托管内存中(在.NET 中,字符串的编码方式默认为Unicode的),即每个字符仅占一个字节,(而对于Unicode编码的字符串来说,字符串中的每个字符都是使用两个字节进行编码的),当非托管函数MessageBoxW开始执行时,它会把该内存中的数据按照Unicode编码处理,即每两个字节当做是一个Unicode字符,知道遇到双字节的‘\0’ 字符结束。所以非托管函数返回的结果也就出现乱码了。 如果指定EntryPoint 字段的值为MessageBoxA,却把CharSet字段设置为CharSet.Unicode的情况下,也会出现同样的乱码问题,如下图所示:
(4). CharSet还有一个可选字段为——CharSet.Auto, 如果把CharSet字段设置为CharSet.Auto,则平台调用会针对目标操作系统适当地自动封送字符串。在 Windows NT、Windows 2000、Windows XP 和 Windows Server 2003 系列上,默认值为 Unicode;在 Windows 98 和 Windows Me 上,默认值为 Ansi。尽管公共语言运行时默认值为 Auto,但使用语言可重写此默认值。例如,默认情况下,C# 将所有方法和类型都标记为 Ansi。所以下面的调用一样也会出现乱码,原因在第三点中已经解释了,下面直接附上测试例子和结果:
class Program { [DllImport("user32.dll", EntryPoint = "MessageBoxA", CharSet = CharSet.Auto)] public static extern int MessageBox5(IntPtr hWnd, String text, String caption, uint type); static void Main(string[] args) { MessageBox5(new IntPtr(0), "Learning Hard", "欢迎", 0); } }
运行结果为:
捕获由Win32函数本身返回异常的演示代码如下:
using System; using System.ComponentModel; // 使用平台调用技术进行互操作性之前,首先需要添加这个命名空间 using System.Runtime.InteropServices; namespace 处理Win32函数返回的错误 { class Program { // Win32 API // DWORD WINAPI GetFileAttributes( // _In_ LPCTSTR lpFileName //); // 在托管代码中对非托管函数进行声明 [DllImport("Kernel32.dll",SetLastError=true,CharSet=CharSet.Unicode)] public static extern uint GetFileAttributes(string filename); static void Main(string[] args) { // 试图获得一个不存在文件的属性 // 此时调用Win32函数会发生错误 GetFileAttributes("FileNotexist.txt"); // 在应用程序的Bin目录下存在一个test.txt文件,此时调用会成功 //GetFileAttributes("test.txt"); // 获得最后一次获得的错误 int lastErrorCode = Marshal.GetLastWin32Error(); // 将Win32的错误码转换为托管异常 //Win32Exception win32exception = new Win32Exception(); Win32Exception win32exception = new Win32Exception(lastErrorCode); if (lastErrorCode != 0) { Console.WriteLine("调用Win32函数发生错误,错误信息为 : {0}", win32exception.Message); } else { Console.WriteLine("调用Win32函数成功,返回的信息为: {0}", win32exception.Message); } Console.Read(); } } }
运行结果为:
要想获得在调用Win32函数过程中出现的错误信息,首先必须将DllImport属性的SetLastError字段设置为true,只有这样,平台调用才会将最后一次调用Win32产生的错误码保存起来,然后会在托管代码调用Win32失败后,通过Marshal类的静态方法GetLastWin32Error获得由平台调用保存的错误码,从而对错误进行相应的分析和处理。这样就可以获得Win32中的错误信息了。
上面代码简单地演示了如何在托管代码中获得最后一次发生的Win32错误信息,然而还可以通过调用Win32 API 提供的FormatMessage函数的方式来获得错误信息,然而这种方式有一个很显然的弊端(所以这里就不演示了),当对FormatMessage函数调用失败时,这时候就有可能获得不正确的错误信息,所以,推荐采用.NET提供的Win32Exception异常类来获得具体的错误信息。关于更多的FormatMessage函数可以参考MSDN: http://msdn.microsoft.com/en-us/library/ms679351(v=vs.85).aspx
三、当调用Win32函数出错时怎么办?——获得Win32函数的错误信息
前面部分为大家演示了平台调用的使用以及使用过程需要注意的问题, 当大家了解了这些之后,肯定会有这样的一个疑问,当调用Win32函数过程中遇到由Win32函数返回的错误要怎样去处理呢? 或者由非托管函数的托管定义导致的错误或异常怎么捕捉,就如上面代码中调用MessageBox1出现异常时,如何捕捉并给用于一个友好的提示信息呢?对于这个两个问题,下面通过两个具体的例子来演示。
捕捉由托管定义导致的异常演示代码:
class Program { // 在托管代码中对非托管函数进行声明,并且附加平台调用所需要属性 // 在默认情况下,CharSet为CharSet.Ansi // 指定调用哪个版本的方法有两种——通过DllImport属性的CharSet字段和通过EntryPoint字段指定 [DllImport("user32.dll")] public static extern int MessageBox1(IntPtr hWnd, String text, String caption, uint type); static void Main(string[] args) { try { MessageBox1(new IntPtr(0), "Learning Hard", "欢迎", 0); } catch (DllNotFoundException dllNotFoundExc) { Console.WriteLine("DllNotFoundException 异常发生,异常信息为: " + dllNotFoundExc.Message); } catch (EntryPointNotFoundException entryPointExc) { Console.WriteLine("EntryPointNotFoundException 异常发生,异常信息为: " + entryPointExc.Message); } Console.Read(); } }
四、小结
讲到这里,本专题的内容也就介绍完了,本专题只是简单介绍了使用平台调用技术来调用Win32函数,然而实际的操作远远不是这么简单的,要掌握平台调用的技术,还需要大家在工作过程多多实践。因为在本专题中涉及了一些数据封送一些知识,为了帮助大家更好掌握数据封送处理,在一个专题将为大家带来平台调用中的数据封送处理专题。运行结果为: