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

摘要:
然而在实际应用中,使用平台调用技术来调用Win32API较为普遍,所以在这个专题中将为大家具体介绍了如何使用平台调用来调用Win32函数以及调用过程中应该注意的问题,下面就从一个具体的实例开始本专题的介绍。然而很多Win32API函数有ANSI和Unicode两个版本并不是随便说说的,而是有根据的。

本文转载自:https://www.cnblogs.com/zhili/archive/2013/01/21/PInvoke.html

本专题概要:

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

一、引言

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

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

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

(1). 获得非托管函数的信息,即dll的名称,需要调用的非托管函数名等信息

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

(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.0Common7ToolsBin” 这个路径找到,然后双击 depends.exe 就可以出来一个可视化界面(如果某些人安装的VS没有附带这个工具,也可以从官方网站下载:http://www.dependencywalker.com/),如下图:

C# 互操作性入门系列(二):使用平台调用调用Win32 函数第1张

上图中 我用红色标示出 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); } } }
复制代码

运行正确的结果为:

C# 互操作性入门系列(二):使用平台调用调用Win32 函数第4张

从代码的注释中可以看出,第一个调用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字段的含义的

C# 互操作性入门系列(二):使用平台调用调用Win32 函数第5张

(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字符,知道遇到双字节的‘

免责声明:内容来源于网络,仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇一篇入门分布式事务框架Seata【AT模式】初中信息技术(Python) 源代码下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

springMVC使用map接收入参 + mybatis使用map 传入查询参数

 测试例子: controllel层 ,使用map接收请求参数,通过Debug可以看到,请求中的参数的值都是字符串形式,如果将这个接收参数的map直接传入service,mybatis接收参数时会报错,因此要先对请求中的参数进行预处理 1 package org.slsale.test; 2 3 import java.util.Date; 4...

Quartz+Spring 实现定时任务的 管理和监控

 0,监控的意义 (1)能够查看有多少定时任务,用的什么执行策略,便于管理 (2)能够通过界面操作停止或启动某个定时任务,便于管理 (3)能够通过界面操作改变某个定时任务的执行策略,便于管理 1,pom.xml 文件添加jar 1 <!-- quartz监控 --> 2 <dependency> 3 <gro...

SortedList、SortedSet、HashSet、Hashtable、Dictionary、SortedDictionary 排序/可重复排序/过滤重复排序等简单对比

//泛型的键值集合/有序/Hash算法/占内存较大/不排序,不受装填因子的限制,对读写操作效率较高 Dictionary<int, string> dc = new Dictionary<int, string>(); dc.Add(1, "111111");...

C# LINQ学习笔记一:走进LINQ的世界

本笔记摘抄自:https://www.cnblogs.com/liqingwen/p/5832322.html,记录一下学习过程以备后续查用。 LINQ 简介: 语言集成查询(LINQ)是Visual Studio 2008和.NET Framework 3.5版中引入的一项创新功能。 传统上,针对数据的查询都是以简单的字符串表示,而没有编译时类型检查或I...

【译】Js基础运行机制

一、js的工作原理:引擎、运行时与调用栈概述 JavaScript引擎的一个流行示例是Google的V8引擎。比如,V8引擎用于Chrome和Node.js。 该引擎包括两个主要组件:*内存堆-这是内存分配的地方*调用堆栈-这是代码执行时堆栈帧的位置 运行时浏览器中有几乎所有JavaScript开发人员都在使用的API(例如“ setTimeout”)。...

DataTable导出到Excel

#region DataTable导出到Excel /// <summary> /// DataTable导出到Excel /// </summary> /// <param name="pData">DataTable</param...