[转]C# 互操做性入门系列(二):使用平台调用调用Win32 函数

传送门html

C#互操做系列文章:windows

  1. C# 互操做性入门系列(一):C#中互操做性介绍
  2. C# 互操做性入门系列(二):使用平台调用调用Win32 函数
  3. C# 互操做性入门系列(三):平台调用中的数据封送处理
  4. C# 互操做性入门系列(四):在C#中调用COM组件

本专题概要:函数

  • 引言
  • 如何使用平台调用Win32 函数——从实例开始
  • 当调用Win32函数出错时怎么办?——得到Win32函数的错误信息
  • 小结

1、引言工具

上一专题对.NET 互操做性作了一个全面的归纳,其中讲到.NET平台下实现互操做性有三种技术——平台调用,C++ Interop和COM Interop,今天在这个专题中将会你们介绍第一种技术,即平台调用。然而朋友们应该会有这样的疑问,平台调用到底有什么用呢? 为何咱们要用平台调用的技术了?对于这两个问题的答案就是——平台调用能够帮助咱们实如今.NET平台下(也就是指用C#、VB.net语言写的应用程序下)能够调用非托管函数(指定的是C/C++语言写的函数)。这样若是咱们在.NET平台下实现的功能有现有的C/C++ 函数实现了这样的功能,这时候咱们彻底不必本身再用托管语言(如C#、vb.net)去实现一个这样的功能,这时候咱们应该想到 “拿来主义”,直接使用平台调用技术调用C/C++ 实现的函数。然而在实际应用中,使用平台调用技术来调用Win32 API较为广泛,因此在这个专题中将为你们具体介绍了如何使用平台调用来调用Win32函数以及调用过程当中应该注意的问题,下面就从一个具体的实例开始本专题的介绍。测试

2、如何使用平台调用Win32 函数——从实例开始网站

在前一个专题中已经介绍了使用平台调用来调用非托管函数的步骤:ui

(1).  得到非托管函数的信息,即dll的名称,须要调用的非托管函数名等信息编码

(2). 在托管代码中对非托管函数进行声明,而且附加平台调用所须要属性spa

(3). 在托管代码中直接调用第二步中声明的托管函数操作系统

然而调用Win32 API函数还有一些问题须要注意的地方, 首先, 由于不少Win32 API函数都有ANSI和Unicode两个版本,因此在托管代码声明时须要指定调用调用函数的版本。  然而不少Win32 API函数有ANSI和Unicode两个版本并非随便说说的,而是有根据的。你们从调用步骤中能够看出,第一步就须要知道非托管函数声明,为了找到须要须要调用的非托管函数,能够借助两个工具——Visual Studio自带的dumpbin.exedepends.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);
         }
     }
运行结果为:

3、当调用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函数自己返回异常的演示代码以下:

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

4、小结

讲到这里,本专题的内容也就介绍完了,本专题只是简单介绍了使用平台调用技术来调用Win32函数,然而实际的操做远远不是这么简单的,要掌握平台调用的技术,还须要你们在工做过程多多实践。由于在本专题中涉及了一些数据封送一些知识,为了帮助你们更好掌握数据封送处理,在一个专题将为你们带来平台调用中的数据封送处理专题。

相关文章
相关标签/搜索