




下载本文档
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、对于DllImport的探讨及其展开DllImport("kernel32.dll")是什么意思?这叫引入kernel32.dll这个动态连接库。这个动态连接库里面包含了很多WindowsAPI函数,如果你想使用这面的函数,就需要这么引入。举个例子:DllImport("kernel32.dll")privatestaticexternvoid函数名(参数,参数);函数名就是一个属于kernel32.dll里的一个函数。完了你就可以用那个函数了。kernel32.dll调用kernel32.dll这个DLL里面的API接口!系统API例如DllImpor
2、t("user32.dll")-引入APIpublicstaticexternReturnTypeFunctionName(typearg1,typearg2,.);/-声明方法调用该方法是和调用普通方法没区别DLLImport属性现在是更深入地进行探讨的时候了。在对托管代码进行P/Invoke调用时,DllImportAttribute类型扮演着重要的角色。DllImportAttribute的主要作用是给CLR指示哪个DLL导出您想要调用的函数。相关DLL的名称被作为一个构造函数参数传递给DllImportAttribute。如果您无法肯定哪个DLL定义了您要使用的Wi
3、ndowsAPI函数,PlatformSDK文档将为您提供最好的帮助资源。在WindowsAPI函数主题文字临近结尾的位置,SDK文档指定了C应用程序要使用该函数必须链接的.lib文件。在几乎所有的情况下,该.lib文件具有与定义该函数的系统DLL文件相同的名称。例如,如果该函数需要C应用程序链接到Kernel32.lib,则该函数就定义在Kernel32.dll中。您可以在MessageBeep中找到有关MessageBeep的PlatformSDK文档主题。在该主题结尾处,您会注意到它指出库文件是User32.lib;这表明MessageBeep是从User32.dll中导出的。可选的Dl
4、lImportAttribute属性除了指出宿主DLL外,DllImportAttribute还包含了一些可选属性,其中四个特别有趣:EntryPoint、CharSet、SetLastError和CallingConvention。EntryPoint在不希望外部托管方法具有与DLL导出相同的名称的情况下,可以设置该属性来指示导出的DLL函数的入口点名称。当您定义两个调用相同非托管函数的外部方法时,这特别有用。另外,在Windows中还可以通过它们的序号值绑定到导出的DLL函数。如果您需要这样做,则诸如“#1或"#129'的EntryPoint值指示DLL中非托管函数的序号
5、值而不是函数名。CharSet对于字符集,并非所有版本的Windows都是同样创建的。Windows9x系列产品缺少重要的Unicode支持,而WindowsNT和WindowsCE系列则一开始就使用Unicodeo在这些操作系统上运行的CLR将Unicode用于String和Char数据的内部表示。但也不必担心一当调用Windows9xAPI函数时,CLR会自动进行必要的转换,将其从Unicode转换为ANSI。如果DLL函数不以任何方式处理文本,则可以忽略DllImportAttribute的CharSet属性。然而,当Char或String数据是等式的一部分时,应该将CharSet属性设
6、置为CharSet.Auto。这样可以使CLR根据宿主OS使用适当的字符集。如果没有显式地设置CharSet属性,则其默认值为CharSet.Ansi。这个默认值是有缺点的,因为对于在Windows2000、WindowsXP和WindowsNT?上进行的interop调用,它会消极地影响文本参数封送处理的性能。应该显式地选择CharSet.Ansi或CharSet.Unicode的CharSet值而不是使用CharSet.Auto的唯一情况是:您显式地指定了一个导出函数,而该函数特定于这两种Win32OS中的某一种。ReadDirectoryChangesWAPI函数就是这样的一个例子,它只
7、存在于基于WindowsNT的操作系统中,并且只支持Unicode;在这种情况下,您应该显式地使用CharSet.Unicode。有时,WindowsAPI是否有字符集关系并不明显。一种决不会有错的确认方法是在PlatformSDK中检查该函数的C语言头文件。(如果您无法肯定要看哪个头文件,则可以查看PlatformSDK文档中列出的每个API函数的头文件。)如果您发现该API函数确实定义为一个映射到以A或W结尾的函数名的宏,则字符集与您尝试调用的函数有关系。WindowsAPI函数的一个例子是在WinUser.h中声明的GetMessageAPI,您也许会惊讶地发现它有A和W两种版本。Set
8、LastError错误处理非常重要,但在编程时经常被遗忘。当您进行P/Invoke调用时,也会面临其他的挑战一处理托管代码中WindowsAPI错误处理和异常之间的区别。我可以给您一点建议。如果您正在使用P/Invoke调用WindowsAPI函数,而对于该函数,您使用GetLastError来查找扩展的错误信息,则应该在外部方法的DllImportAttribute中将SetLastError属性设置为true。这适用于大多数外部方法。这会导致CLR在每次调用外部方法之后缓存由API函数设置的错误。然后,在包装方法中,可以通过调用类库的System.Runtime.InteropServic
9、es.Marshal类型中定义的Marshal.GetLastWin32Error方法来获取缓存的错误值。我的建议是检查这些期望来自API函数的错误值,并为这些值引发一个可感知的异常。对于其他所有失败情况(包括根本就没意料到的失败情况),则引发在System.ComponentModel命名空间中定义的Win32Exception,并将Marshal.GetLastWin32Error返回的值传递给它。如果您回头看一下图1中的代码,您会看到我在externMessageBeep方法的公共包装中就采用了这种方法。CallingConvention我将在此介绍的最后也可能是最不重要的一个DllIm
10、portAttribute属性是CallingConvention。通过此属性,可以给CLR指示应该将哪种函数调用约定用于堆栈中的参数。CallingConvention.Winapi的默认值是最好的选择,它在大多数情况下都可行。然而,如果该调用不起作用,则可以检查PlatformSDK中的声明头文件,看看您调用的API函数是否是一个不符合调用约定标准的异常API。通常,本机函数(例如WindowsAPI函数或C-运行时DLL函数)的调用约定描述了如何将参数推入线程堆栈或从线程堆栈中清除。大多数WindowsAPI函数都是首先将函数的最后一个参数推入堆栈,然后由被调用的函数负责清理该堆栈。相反
11、,许多C-运行时DLL函数都被定义为按照方法参数在方法签名中出现的顺序将其推入堆栈,将堆栈清理工作交给调用者。幸运的是,要让P/Invoke调用工作只需要让外围设备理解调用约定即可。通常,从默认值CallingConvention.Winapi开始是最好的选择。然后,在C运行时DLL函数和少数函数中,可能需要将约定更改为CallingConvention.Cdecl。C#API大全C#APIC:ProgramFilesMicrosoftVisualStudio.NETFrameworkSDKSamplesTechnologiesInteropPlatformInvokeWinAPIsCS目录下
12、有大量的调用API的例子。一、调用格式usingSystem.Runtime.InteropServices;/引用此名称空间,简化后面的代码/使用DllImportAttribute特性来引入api函数,注意声明的是空方法,即方法体为空。DllImport("user32.dll")publicstaticexternReturnTypeFunctionName(typearg1,typearg2,.);调用时与调用其他方法并无区别可以使用字段进一步说明特性,用逗号隔开,如:DllImport("kernel32",EntryPoint="G
13、etVersionEx")DllImportAttribute特性的公共字段如下:1、CallingConvention指示向非托管实现传递方法参数时所用的CallingConvention值。CallingConvention.Cdecl:调用方清理堆栈。它使您能够调用具有varargs的函数。CallingConvention.StdCall:被调用方清理堆栈。它是从托管代码调用非托管函数的默认约定。2、CharSet控制调用函数的名称版本及指示如何向方法封送String参数。此字段被设置为CharSet值之一。如果CharSet字段设置为Unicode,则所有字符串参数在传递到
14、非托管实现之前都转换成Unicode字符。这还导致向DLLEntryPoint的名称中追加字母"W5如果此字段设置为Ansi,则字符串将转换成ANSI字符串,同时向DLLEntryPoint的名称中追加字母"A'大多数Win32API使用这种追加“尚“陆约定。如果CharSet设置为Auto,则这种转换就是与平台有关的(在WindowsNT上为Unicode,在Windows98上为Ansi)。CharSet的默认值为Ansi。CharSet字段也用于确定将从指定的DLL导入哪个版本的函数。CharSet.Ansi和CharSet.Unicode的名称匹配规则大不相
15、同。对于Ansi来说,如果将EntryPoint设置为"MyMethod且它存在的话,则返回"MyMethod"。如果DLL中没有"MyMethod",但存在"MyMethodA',贝U返回"MyMethodA'。对于Unicode来说则正好相反。如果将EntryPoint设置为"MyMethod且它存在的话,则返回“MyMethodW。如果DLL中不存在"MyMethodW,但存在“MyMethod,贝U返回“MyMethod”。如果使用的是Auto,则匹配规则与平台有关(在Window
16、sNT上为Unicode,在Windows98上为Ansi)。如果ExactSpelling设置为true,则只有当DLL中存在“MyMethod时才返回“MyMethod。3、EntryPoint指示要调用的DLL入口点的名称或序号。如果你的方法名不想与api函数同名的话,一定要指定此参数,例如:DllImport("user32.dll",CharSet="CharSet.Auto",EntryPoint="MessageBox")publicstaticexternintMsgBox(IntPtrhWnd,stringtxt,s
17、tringcaption,inttype);4、ExactSpelling指示是否应修改非托管DLL中的入口点的名称,以与CharSet字段中指定的CharSet值相对应。如果为true,则当DllImportAttribute.CharSet字段设置为CharSet的Ansi值时,向方法名称中追加字母A,当DllImportAttribute.CharSet字段设置为CharSet的Unicode值时,向方法的名称中追加字母W。此字段的默认值是false。5、PreserveSig指示托管方法签名不应转换成返回HRESULT、并且可能有一个对应于返回值的附加out,retval参数的非托管签
18、名。6、SetLastError指示被调用方在从属性化方法返回之前将调用Win32APISetLastError。true指示调用方将调用SetLastError,默认为false。运行时封送拆收器将调用GetLastError并缓存返回的值,以防其被其他API调用重写。用户可通过调用GetLastWin32Error来检索错误代码。二、参数类型:1、数值型直接用对应的就可。(DWORD->int,WORD->Int16)2、API中字符串指针类型->.net中string3、API中句柄(dWord)->.net中IntPtr4、API中结构->.net中结构或
19、者类。注意这种情况下,要先用StructLayout特性限定声明结构或类公共语言运行库利用StructLayoutAttribute控制类或结构的数据字段在托管内存中的物理布局,即类或结构需要按某种方式排列。如果要将类传递给需要指定布局的非托管代码,则显式控制类布局是重要的。它的构造函数中用LayoutKind值初始化StructLayoutAttribute类的新实例。LayoutKind.Sequential用于强制将成员按其出现的顺序进行顺序布局。LayoutKind.Explicit用于控制每个数据成员的精确位置。利用Explicit,每个成员必须使用FieldOffsetAttrib
20、ute指示此字段在类型中的位置。如:StructLayout(LayoutKind.Explicit,Size=16,CharSet=CharSet.Ansi)publicclassMySystemTimeFieldOffset(0)publicushortwYear;FieldOffset(2)publicushortwMonth;FieldOffset(4)publicushortwDayOfWeek;FieldOffset(6)publicushortwDay;FieldOffset(8)publicushortwHour;FieldOffset(10)publicushortwMinu
21、te;FieldOffset(12)publicushortwSecond;FieldOffset(14)publicushortwMilliseconds;卜面是针对API中OSVERSIONINFO结构,在.net中定义对应类或结构的例子:/* API中定义原结构声明* OSVERSIONINFOASTRUCT* dwOSVersionInfoSizeDWORD?* dwMajorVersionDWORD?* dwMinorVersionDWORD?* dwBuildNumberDWORD?* dwPlatformIdDWORD?* szCSDVersionBYTE128dup(?)* O
22、SVERSIONINFOAENDS* OSVERSIONINFOequ<OSVERSIONINFOA>*I/.net中声明为类StructLayout(LayoutKind.Sequential)publicclassOSVersionInfo(publicintOSVersionInfoSize;publicintmajorVersion;publicintminorVersion;publicintbuildNumber;publicintplatformId;MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)publicStrin
23、gversionString;或者/.net中声明为结构StructLayout(LayoutKind.Sequential)publicstructOSVersionInfo2publicintOSVersionInfoSize;publicintmajorVersion;publicintminorVersion;publicintbuildNumber;publicintplatformId;MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)publicStringversionString;此例中用到MashalAs特性,它用于描述字段、方
24、法或参数的封送处理格式。用它作为参数前缀并指定目标需要的数据类型。例如,以下代码将两个参数作为数据类型长指针封送给WindowsAPI函数的字符串(LPStr):MarshalAs(UnmanagedType.LPStr)Stringexistingfile;MarshalAs(UnmanagedType.LPStr)Stringnewfile;注意结构作为参数时候,一般前面要加上ref修饰符,否则会出现错误:对象的引用没有指定对象的实例。DllImport("kernel32",EntryPoint="GetVersionEx")publicstati
25、cexternboolGetVersionEx2(refOSVersionInfo2osvi);三、如何保证使用托管对象的平台调用成功?如果在调用平台invoke后的任何位置都未引用托管对象,则垃圾回收器可能将完成该托管对象。这将释放资源并使句柄无效,从而导致平台invoke调用失败。用HandleRef包装句柄可保证在平台invoke调用完成前,不对托管对象进行垃圾回收。例如下面:FileStreamfs=newFileStream("a.txt",FileMode.Open);StringBuilderbuffer=newStringBuilder(5);intread
26、=0;ReadFile(fs.Handle,buffer,5,outread,0);/调用WinAPI中的ReadFile函数由于fs是托管对象,所以有可能在平台调用还未完成时候被垃圾回收站回收。将文件流的句柄用HandleRef包装后,就能避免被垃圾站回收:DllImport("Kernel32.dll")publicstaticexternboolReadFile(HandleRefhndRef,StringBuilderbuffer,intnumberOfBytesToRead,outintnumberOfBytesRead,refOverlappedflag);Fi
27、leStreamfs=newFileStream("HandleRef.txt",FileMode.Open);HandleRefhr=newHandleRef(fs,fs.Handle);StringBuilderbuffer=newStringBuilder(5);intread=0;/platforminvokewillholdreferencetoHandleRefuntilcallendsReadFile(hr,buffer,5,outread,0);我在自己最近的编程中注意到一个趋势,正是这个趋势才引出本月的专栏主题。最近,我在基于Microsoft?.NETF
28、ramework的应用程序中完成了大量的Win32?Interop。我并不是要说我的应用程序充满了自定义的interop代码,但有时我会在.NETFramework类库中碰到一些次要但又繁絮、不充分的内容,通过调用该Windows?API,可以快速减少这样的麻烦。因此我认为,.NETFramework1.0或1.1版类库中存在任何Windows所没有的功能限制都不足为怪。毕竟,32位的Windows(不管何种版本)是一个成熟的操作系统,为广大客户服务了十多年。相比之下,.NETFramework却是一个新事物。开发人员更频繁地研究底层操作系随着越来越多的开发人员将生产应用程序转到托管代码,统以
29、图找出一些关键功能显得很自然一至少目前是如此。值得庆幸的是,公共语言运行库(CLR)的interop功能(称为平台调用(P/Invoke)非常完善。在本专栏中,我将重点介绍如何实际使用P/Invoke来调用WindowsAPI函数。当指CLR的COMInterop功能时,P/Invoke当作名词使用;当指该功能的使用时,则将其当作动词使用。我并不打算直接介绍COMInterop,因为它比P/Invoke具有更好的可访问性,却更加复杂,这有点自相矛盾,这使得将COMInterop作为专栏主题来讨论不太简明扼要。走进P/Invoke首先从考察一个简单的P/Invoke示例开始。让我们看一看如何调用
30、Win32MessageBeep函数,它的非托管声明如以下代码所示:BOOLMessageBeep(UINTuType/beeptype);为了调用MessageBeep,您需要在C#中将以下代码添加到一个类或结构定义中:DllImport("User32.dll")staticexternBooleanMessageBeep(UInt32beepType);令人惊讶的是,只需要这段代码就可以使托管代码调用非托管的MessageBeepAPI。它不是一个方法调用,而是一个外部方法定义。(另外,它接近于一个来自C而C#允许的直接端口,因此以它为起点来介绍一些概念是有帮助的。)
31、来自托管代码的可能调用如下所示:MessageBeep(0);请注意,现在MessageBeep方法被声明为static。这是P/Invoke方法所要求的,因为在t亚WindowsAPI中没有一致的实例概念。接下来,还要注意该方法被标记为extern。这是提示编译器该方法是通过一个从DLL导出的函数实现的,因此不需要提供方法体。说到缺少方法体,4是否注意到MessageBeep声明并没有包含一个方法体?与大多数算法由中间语言(IL)指令组成的托管方法不同,P/Invoke方法只是元数据,实时(JIT)编译器在运行时通过它将托管代码与非托管的DLL函数连接起来。执行这种到非托管世界的连接所需的一
32、个重要信息就是导出非托管方法的DLL的名称。这一信息是由MessageBeep方法声明之前的DllImport自定义属性提供的。在本例中,可以看到,MessageBeep非托管API是由Windows中的User32.dll导出的。到现在为止,关于调用MessageBeep就剩两个话题没有介绍,请回顾一下,调用的代码与以下所示代码片段非常相似:DllImport("User32.dll")staticexternBooleanMessageBeep(UInt32beepType);最后这两个话题是与数据封送处理(datamarshaling)和从托管代码到非托管函数的实际方
33、法调用有关的话题。调用非托管MessageBeep函数可以由找到作用域内的extern它与MessageBeep声明的任何托管代码执行。该调用类似于任何其他对静态方法的调用。其他任何托管方法调用的共同之处在于带来了数据封送处理的需要。C#的规则之一是它的调用语法只能访问CLR数据类型,例如System.UInt32和System.Boolean°C#显然不识别WindowsAPI中使用的基于C的数据类型(例如UINT和BOOL),这些类型只是C语言类型的类型定义而已。所以当WindowsAPI函数MessageBeep按以下方式编写时BOOLMessageBeep(UINTuType
34、)外部方法就必须使用CLR类型来定义,如您在前面的代码片段中所看到的。需要使用与基础API函数类型不同但与之兼容的CLR类型是P/Invoke较难使用的一个方面。因此,在本专栏的后面我将用完整的章节来介绍数据封送处理。样式在C#中对WindowsAPI进彳tP/Invoke调用是很简单的。但如果类库拒绝使您的应用程序发出嘟声,应该想方设法调用Windows使它进行这项工作,是吗?是的。但是与选择的方法有关,而且关系甚大!通常,如果类库提供某种途径来实现您的意图,则最好使用API而不要直接调用非托管代码,因为CLR类型和Win32之间在样式上有很大的不同。我可以将关于这个问题的建议归结为一句话。
35、当您进行P/Invoke时,不要使应用程序逻辑直接属于任何外部方法或其中的构件。如果您遵循这个小规则,从长远看经常会省去许多的麻烦。图1中的代码显示了我所讨论的MessageBeep外部方法的最少附加代码。图1中并没有任何显著的变化,而只是对无包装的外部方法进行一些普通的改进,这可以使工作更加轻松一些。从顶部开始,您会注意到一个名为Sound的完整类型,它专用于MessageBeep(如果我需要使用WindowsAPI函数PlaySound来添加对播放波形的支持,则可以重用Sound类型。然而,我不会因公开单个公共静态方法的类型而生气。毕竟这只是应用程序代码而已。还应该注意到,Sound是密封
36、的,并定义了一个空的私有构造函数。这些只是一些细节,目的是使用户不会错误地从Sound派生类或者创建它的实例。图1中的代码的下一个特征是,P/Invoke出现位置的实际外部方法是Sound的私有方法。这个方法只是由公共MessageBeep方法间接公开,后者接受BeepTypes类型的参数。这个间接的额外层是一个很关键的细节,它提供了以下好处。首先,应该在类库中引入一个未来的beep托管方法,可以重复地通过公共MessageBeep方法来使用托管API,而不必更改应用程序中的其余代码。该包装方法的第二个好处是:当您进行P/Invoke调用时,您放弃了免受访问冲突和其他低级破坏的权利,这通常是由
37、CLR提供的。缓冲方法可以保护您的应用程序的其余部分免受访问冲突及类似问题的影响(即使它不做任何事而只是传递参数)。该缓冲方法将由P/Invoke调用引入的任何潜在的错误本地化。将私有外部方法隐藏在公共包装后面的第三同时也是最后的一个好处是,提供了向该方法添加一些最小的CLR样式的机会。例如,在图1中,我将WindowsAPI函数返回的Boolean失败转换成更像CLR的异常。我还定义了一个名为BeepTypes的枚举类型,它的成员对应于同该WindowsAPI一起使用的定义值。由于C#不支持定义,因此可以使用托管枚举类型来避免幻数向整个应用程序代码扩散。包装方法的最后一个女?处对于简单的Wi
38、ndowsAPI函数(如MessageBeep)诚然是微不足道的。但是当您开始调用更复杂的非才管函数时,您会发现,手动将WindowsAPI样式转换成对CLR更加友好的方法所带来的好处会越来越多。越是打算在整个应用程序中重用interop功能,越是应该认真地考虑包装的设计。同时我认为,在非面向对象的静态包装方法中使用对CLR友好的参数也并非不可以。DLLImport属性现在是更深入地进行探讨的时候了。在对托管代码进行P/Invoke调用时,DllImportAttribute类型扮演着重要的角色。DllImportAttribute的主要作用是给CLR指示哪个DLL导出您想要调用的函数。相关D
39、LL的名称被作为一个构造函数参数传递给DllImportAttribute。如果您无法肯定哪个DLL定义了您要使用的WindowsAPI函数,PlatformSDK文档将为您提供最好的帮助资源。在WindowsAPI函数主题文字临近结尾的位置,SDK文档指定了C应用程序要使用该函数必须链接的.lib文件。在几乎所有的情况下,该.lib文件具有与定义该函数的系统DLL文件相同的名称。例如,如果该函数需要C应用程序链接到Kernel32.lib,则该函数就定义在Kernel32.dll中。您可以在MessageBeep中找到有关MessageBeep的PlatformSDK文档主题。在该主题结尾处
40、,您会注意到它指出库文件是User32.lib;这表明MessageBeep是从User32.dll中导出的。可选的DllImportAttribute属性除了指出宿主DLL外,DllImportAttribute还包含了一些可选属性,其中四个特别有趣:EntryPoint、CharSet、SetLastError和CallingConvention。EntryPoint在不希望外部托管方法具有与DLL导出相同的名称的情况下,可以设置该属性来指示导出的DLL函数的入口点名称。当您定义两个调用相同非托管函数的外部方法时,这特别有用。另外,在Windows中还可以通过它们的序号值绑定到导出的DLL
41、函数。如果您需要这样做,则诸如“#1或"#129的EntryPoint值指示DLL中非托管函数的序号值而不是函数名。CharSet对于字符集,并非所有版本的Windows都是同样创建的。Windows9x系列产品缺少重要的Unicode支持,而WindowsNT和WindowsCE系列则一开始就使用Unicode。在这些操作系统上运行的CLR将Unicode用于String和Char数据的内部表示。但也不必担心一当调用Windows9xAPI函数时,CLR会自动进行必要的转换,将其从Unicode转换为ANSI。如果DLL函数不以任何方式处理文本,则可以忽略DllImportAttr
42、ibute的CharSet属性。然而,当Char或String数据是等式的一部分时,应该将CharSet属性设置为CharSet.Auto。这样可以使CLR根据宿主OS使用适当的字符集。如果没有显式地设置CharSet属性,则其默认值为CharSet.Ansi。这个默认值是有缺点的,因为对于在Windows2000>WindowsXP和WindowsNT?上进行的interop调用,它会消极地影响文本参数封送处理的性能。应该显式地选择CharSet.Ansi或CharSet.Unicode的CharSet值而不是使用CharSet.Auto的唯一情况是:您显式地指定了一个导出函数,而该函
43、数特定于这两种Win32OS中的某一种。ReadDirectoryChangesWAPI函数就是这样的一个例子,它只存在于基于WindowsNT的操作系统中,并且只支持Unicode;在这种情况下,您应该显式地使用CharSet.Unicode。有时,WindowsAPI是否有字符集关系并不明显。一种决不会有错的确认方法是在PlatformSDK中检查该函数的C语言头文件。(如果您无法肯定要看哪个头文件,则可以查看PlatformSDK文档中列出的每个API函数的头文件。)如果您发现该API函数确实定义为一个映射到以A或W结尾的函数名的宏,则字符集与您尝试调用的函数有关系。WindowsAPI
44、函数的一个例子是在WinUser.h中声明的GetMessageAPI,您也许会惊讶地发现它有A和W两种版本。SetLastError错误处理非常重要,但在编程时经常被遗忘。当您进行P/Invoke调用时,也会面临其他的挑战一处理托管代码中WindowsAPI错误处理和异常之间的区别。我可以给您一点建议。如果您正在使用P/Invoke调用WindowsAPI函数,而对于该函数,您使用GetLastError来查找扩展的错误信息,则应该在外部方法的DllImportAttribute中将SetLastError属性设置为true。这适用于大多数外部方法。这会导致CLR在每次调用外部方法之后缓存由
45、API函数设置的错误。然后,在包装方法中,可以通过调用类库的System.Runtime.InteropServices.Marshal类型中定义的Marshal.GetLastWin32Error方法来获取缓存的错误值。我的建议是检查这些期望来自API函数的错误值,并为这些值引发一个可感知的异常。对于其他所有失败情况(包括根本就没意料到的失败情况),则引发在System.ComponentModel命名空间中定义的Win32Exception,并将Marshal.GetLastWin32Error返回的值传递给它。如果您回头看一下图1中的代码,您会看到我在externMessageBeep方
46、法的公共包装中就采用了这种方法。Callingconvention我将在此介绍的最后也可能是最不重要的一个DllImportAttribute属性是Callingconvention。通过此属性,可以给CLR指示应该将哪种函数调用约定用于堆栈中的参数。CallingConvention.Winapi的默认值是最好的选择,它在大多数情况下都可行。然而,如果该调用不起作用,则可以检查PlatformSDK中的声明头文件,看看您调用的API函数是否是一个不符合调用约定标准的异常API。通常,本机函数(例如WindowsAPI函数或C-运行时DLL函数)的调用约定描述了如何将参数推入线程堆栈或从线程堆
47、栈中清除。大多数WindowsAPI函数都是首先将函数的最后一个参数推入堆栈,然后由被调用的函数负责清理该堆栈。相反,许多C-运行时DLL函数都被定义为按照方法参数在方法签名中出现的顺序将其推入堆栈,将堆栈清理工作交给调用者。幸运的是,要让P/Invoke调用工作只需要让外围设备理解调用约定即可。通常,从默认值CallingConvention.Winapi开始是最好的选择。然后,在C运行时DLL函数和少数函数中,可能需要将约定更改为CallingConvention.Cdecl。数据封送处理数据封送处理是P/Invoke具有挑战性的方面。当在托管和非托管代码之间传递数据时,CLR遵循许多规则
48、,很少有开发人员会经常遇到它们直至可将这些规则记住。除非您是一名类库开发人员,否则在通常情况下没有必要掌握其细节。为了最有效地在CLR上使用P/Invoke,即使只偶尔需要interop的应用程序开发人员仍然应该理解数据封送处理的一些基础知识。在本月专栏的剩余部分中,我将讨论简单数字和字符串数据的数据封送处理。我将从最基本的数字数据封送处理开始,然后介绍简单的指针封送处理和字符串封送处理。封送数字和逻辑标量WindowsOS大部分是用C编写的。因此,WindowsAPI所用到的数据类型要么是C类型,要么是通过类型定义或宏定义重新标记的C类型。让我们看看没有指针的数据封送处理。简单起见,首先重点
49、讨论的是数字和布尔值。当通过值向WindowsAPI函数传递参数时,需要知道以下问题的答案:?数据从根本上讲是整型的还是浮点型的??如果数据是整型的,则它是有符号的还是无符号的??如果数据是整型的,则它的位数是多少??如果数据是浮点型的,则它是单精度的还是双精度的?有时答案很明显,但有时却不明显。WindowsAPI以各种方式重新定义了基本的C数据类型。图2列出了C和Win32的一些公共数据类型及其规范,以及一个具有匹配规范的公共语言运行库类型。通常,只要您选择一个其规范与该参数的Win32类型相匹配的CLR类型,您的代码就能够正常工作。不过也有一些特例。例如,在WindowsAPI中定义的B
50、OOL类型是一个有符号的32位整型。然而,BOOL用于指示Boolean值true或false。虽然您不用将BOOL参数作为System.Int32值封送,但是如果使用System.Boolean类型,就会获得更合适的映射。字符类型的映射类似于BOOL,因为有一个特定的CLR类型(System.Char)指出字符的含义。在了解这些信息之后,逐步介绍示例可能是有帮助的。依然采用beep主题作为例子,让我们来试一下Kernel32.dll低级Beep,它会通过计算机的扬声器发生嘟声。这个方法的PlatformSDK文档可以在Beep中找到。本机API按以下方式进行记录:BOOLBeep(DWORD
51、dwFreq,/FrequencyDWORDdwDuration/Durationinmilliseconds);在参数封送处理方面,您的工作是了解什么CLR数据类型与BeepAPI函数所使用的DWORD和BOOL数据类型相兼容。回顾一下图2中的图表,您将看到DWORD是一个32位的无符号整数值,如同CLR类型System.UInt32。这意味着您可以使用UInt32值作为送往Beep的两个参数。BOOL返回值是一个非常有趣的情况,因为该图表告诉我们,在Win32中,BOOL是一个32位的有符号整数。因此,您可以使用System.Int32值作为来自Beep的返回值。然而,CLR也定义了Sys
52、tem.Boolean类型作为Boolean值的语义,所以应该使用它来替代。CLR默认将System.Boolean值封送为32位的有符号整数。此处所显示的外部方法定义是用于Beep的结果P/Invoke方法:DllImport("Kernel32.dll",SetLastError=true)staticexternBooleanBeep(UInt32frequency,UInt32duration);指针参数许多WindowsAPI函数将指针作为它们的一个或多个参数。指针增加了封送数据的复杂性,因为它们增加了一个间接层。如果没有指针,您可以通过值在线程堆栈中传递数据。有
53、了指针,则可以通过引用传递数据,方法是将该数据的内存地址推入线程堆栈中。然后,函数通过内存地址间接访问数据。使用托管代码表示此附加间接层的方式有多种。在C#中,如果将方法参数定义为ref或out,则数据通过引用而不是通过值传递。即使您没有使用Interop也是这样,但只是从一个托管方法调用到另一个托管方法。例如,如果通过ref传递System.Int32参数,则在线程堆栈中传递的是该数据的地址,而不是整数值本身。下面是一个定义为通过引用接收整数值的方法的示例:voidFlipInt32(refInt32num)num=-num;这里,FlipInt32方法获取一个Int32值的地址、访问数据、
54、对它求反,然后将求反过的值赋给原始变量。在以下代码中,FlipInt32方法会将调用程序的变量x的值从10更改为-10:Int32x=10;FlipInt32(refx);在托管代码中可以重用这种能力,将指针传递给非托管代码。例如,FileEncryptionStatusAPI函数以32位无符号位掩码的形式返回文件加密状态。该API按以下所示方式进行记录:BOOLFileEncryptionStatus(LPCTSTRlpFileName,/filenameLPDWORDlpStatus/encryptionstatus);请注意,该函数并不使用它的返回值返回状态,而是返回一个Boolean值
55、,指示调用是否成功。在成功的情况下,实际的状态值是通过第二个参数返回的。它的工作方式是调用程序向该函数传递指向一个DWORD变量的指针,而该API函数用状态值填充指向的内存位置。以下代码片段显示了一个调用非托管FileEncryptionStatus函数的可能外部方法定义:DllImport("Advapi32.dll",CharSet=CharSet.Auto)staticexternBooleanFileEncryptionStatus(Stringfilename,outUInt32status);该定义使用out关键字来为UInt32状态值指示by-ref参数。这里
56、我也可以选择ref关键字,实际上在运行时会产生相同的机器码。out关键字只是一个by-ref参数的规范,它向C#编译器指示所传递的数据只在被调用的函数外部传递。相反,如果使用ref关键字,则编译器会假定数据可以在被调用的函数的内部和外部传递。托管代码中out和ref参数的另一个很好的方面是,地址作为by-ref参数传递的变量可以是线程堆栈中的一个本地变量、一个类或结构的元素,也可以是具有合适数据类型的数组中的一个元素引用。调用程序的这种灵活性使得by-ref参数成为封送缓冲区指针以及单数值指针的一个很好的起点。只有在我发现ref或out参数不符合我的需要的情况下,我才会考虑将指针封送为更复杂的
57、CLR类型(例如类或数组对象)。如果您不熟悉C语法或者调用WindowsAPI函数,有时很难知道一个方法参数是否需要指针。一个常见的指示符是看参数类型是否是以字母P或LP开头的,例如LPDWORD或PINT。在这两个例子中,LP和P指示参数是一个指针,而它们指向的数据类型分别为DWORD或INT。然而,在有些情况下,可以直接使用C语言语法中的星号(*)将API函数定义为指针。以下代码片段展示了这方面的示例:voidTakesAPointer(DWORD*pNum);可以看到,上述函数的唯个参数是指向DWORD变量的指针。当通过P/Invoke封送指针时,ref和out只用于托管代码中的值类型。当一个参数的CLR类型使用struct关键字定义时,可以认为该参数是一个值类型。Out和ref用于封送指向这些数据类型的指针,因为通常值类型变量是对象或数据,而在托管代码中并没有对值类型的引用。相反,当封送引用类型对象时,并不需要ref和out关键字,因为变量已经是对象的引用了。如果您对引用类型和值类型之间的差别不是很熟悉,请
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025年X射线高频高压发生装置合作协议书
- 2025年板材无模多点成型压力机项目发展计划
- 2025年枣阳市法院系统招聘真题
- 2025年宝鸡市市级机关公开遴选考试真题
- 土地使用合同四篇
- 2025福建省晋江圳源环境科技有限责任公司招聘6人模拟试卷及答案详解(历年真题)
- 2025年济柴动力有限公司春季高校毕业生招聘(10人)模拟试卷及答案详解参考
- 食品加工协议书范本5篇
- 2025广西百色西林县地方志编纂服务中心公开招聘1人考前自测高频考点模拟试题及一套参考答案详解
- 2025广东佛山市中心血站南海血站招聘公益一类事业编制工作人员2人考前自测高频考点模拟试题附答案详解(突破训练)
- 一国两制课件
- 2025年全国国家版图知识竞赛题库及答案(中小学组)
- 十一节后收心会安全培训课件
- 医院麻醉药品、第一类精神药品注射剂空安瓿回收登记表
- 研究借鉴晋江经验-加快构建三条战略通道
- 他克莫司治疗肾病综合征优势课件
- 新版GMP教程第五章设备课件
- 99S203 消防水泵接合器安装图集
- 轴承故障诊断演示文稿
- 高原性红细胞增多症的观察和护理
- 大连理工.电机与拖动PPT课件11章全744P
评论
0/150
提交评论