在C# 中通过 PInvoke 调用Win32 DLL

来源:互联网 发布:上海万国数据待遇 编辑:程序博客网 时间:2024/05/22 15:30

我在自己最近的编程中注意到一个趋势,正是这个趋势才引出本月的专栏主题。最近,我在基于Microsoft.NETFramework的应用程序中完成了大量的Win32Interop。我并不是要说我的应用程序充满了自定义的interop代码,但有时我会在.NETFramework类库中碰到一些次要但又繁絮、不充分的内容,通过调用该WindowsAPI,可以快速减少这样的麻烦。

  因此我认为,.NETFramework1.0或1.1版类库中存在任何Windows所没有的功能*都不足为怪。毕竟,32位的Windows(不管何种版本)是一个成熟的操作系统,为广大客户服务了十多年。相比之下,.NETFramework却是一个新事物。

  随着越来越多的开发人员将生产应用程序转到托管代码,开发人员更频繁地研究底层操作系统以图找出一些关键功能显得很自然-至少目前是如此。

  值得庆幸的是,公共语言运行库(CLR)的interop功能(称为平台调用(P/Invoke))非常完善。在本专栏中,我将重点介绍如何实际使用P/Invoke来调用WindowsAPI函数。当指CLR的COMInterop功能时,P/Invoke当作名词使用;当指该功能的使用时,则将其当作动词使用。我并不打算直接介绍COMInterop,因为它比P/Invoke具有更好的可访问性,却更加复杂,这有点自相矛盾,这使得将COMInterop作为专栏主题来讨论不太简明扼要。

  诛仙-在C中通过PInvoke调用Win32DLL。

  走进P/Invoke。

  首先从考察一个简单的P/Invoke示例开始。让我们看一看如何调用Win32MessageBeep函数,它的非托管声明如以下代码所示:BOOLMessageBeep(UINTuType//beeptype);。

  为了调用MessageBeep,您需要在C#中将以下代码添加到一个类或结构定义中:[DllImport("User32.dll")]staticexternBooleanMessageBeep(UInt32beepType);。

  令人惊讶的是,只需要这段代码就可以使托管代码调用非托管的MessageBeepAPI。它不是一个方法调用,而是一个外部方法定义。(另外,它接近于一个来自C而C#允许的直接端口,因此以它为起点来介绍一些概念是有帮助的。)来自托管代码的可能调用如下所示:MessageBeep(0);。

  诛仙-在C中通过PInvoke调用Win32DLL。

  请注意,现在MessageBeep方法被声明为static。这是P/Invoke方法所要求的,因为在该WindowsAPI中没有一致的实例概念。接下来,还要注意该方法被标记为extern。这是提示编译器该方法是通过一个从DLL导出的函数实现的,因此不需要提供方法体。

  说到缺少方法体,您是否注意到MessageBeep声明并没有包含一个方法体?与大多数算法由中间语言(IL)指令组成的托管方法不同,P/Invoke方法只是元数据,实时(JIT)编译器在运行时通过它将托管代码与非托管的DLL函数连接起来。执行这种到非托管世界的连接所需的一个重要信息就是导出非托管方法的DLL的名称。这一信息是由MessageBeep方法声明之前的DllImport自定义属性提供的。在本例中,可以看到,MessageBeep非托管API是由Windows中的User32.dll导出的。

  到现在为止,关于调用MessageBeep就剩两个话题没有介绍,请回顾一下,调用的代码与以下所示代码片段非常相似:[DllImport("User32.dll")]staticexternBooleanMessageBeep(UInt32beepType);。

  最后这两个话题是与数据封送处理(datamarshaling)和从托管代码到非托管函数的实际方法调用有关的话题。调用非托管MessageBeep函数可以由找到作用域内的externMessageBeep声明的任何托管代码执行。该调用类似于任何其他对静态方法的调用。它与其他任何托管方法调用的共同之处在于带来了数据封送处理的需要。

  C#的规则之一是它的调用语法只能访问CLR数据类型,例如System.UInt32和System.Boolean。C#显然不识别WindowsAPI中使用的基于C的数据类型(例如UINT和BOOL),这些类型只是C语言类型的类型定义而已。所以当WindowsAPI函数MessageBeep按以下方式编写时BOOLMessageBeep(UINTuType)。

  外部方法就必须使用CLR类型来定义,如您在前面的代码片段中所看到的。需要使用与基础API函数类型不同但与之兼容的CLR类型是P/Invoke较难使用的一个方面。因此,在本专栏的后面我将用完整的章节来介绍数据封送处理。

  样式。

  在C#中对WindowsAPI进行P/Invoke调用是很简单的。但如果类库拒绝使您的应用程序发出嘟声,应该想方设法调用Windows使它进行这项工作,是吗?。

  是的。但是与选择的方法有关,而且关系甚大!通常,如果类库提供某种途径来实现您的意图,则最好使用API而不要直接调用非托管代码,因为CLR类型和Win32之间在样式上有很大的不同。我可以将关于这个问题的建议归结为一句话。当您进行P/Invoke时,不要使应用程序逻辑直接属于任何外部方法或其中的构件。如果您遵循这个小规则,从长远看经常会省去许多的麻烦。

  图1中的代码显示了我所讨论的MessageBeep外部方法的最少附加代码。图1中并没有任何显著的变化,而只是对无包装的外部方法进行一些普通的改进,这可以使工作更加轻松一些。从顶部开始,您会注意到一个名为Sound的完整类型,它专用于MessageBeep。如果我需要使用WindowsAPI函数PlaySound来添加对播放波形的支持,则可以重用Sound类型。然而,我不会因公开单个公共静态方法的类型而生气。毕竟这只是应用程序代码而已。还应该注意到,Sound是密封的,并定义了一个空的私有构造函数。这些只是一些细节,目的是使用户不会错误地从Sound派生类或者创建它的实例。

  图1中的代码的下一个特征是,P/Invoke出现位置的实际外部方法是Sound的私有方法。这个方法只是由公共MessageBeep方法间接公开,后者接受BeepTypes类型的参数。这个间接的额外层是一个很关键的细节,它提供了以下好处。首先,应该在类库中引入一个未来的beep托管方法,可以重复地通过公共MessageBeep方法来使用托管API,而不必更改应用程序中的其余代码。

  该包装方法的第二个好处是:当您进行P/Invoke调用时,您放弃了免受访问冲突和其他低级破坏的权利,这通常是由CLR提供的。缓冲方法可以保护您的应用程序的其余部分免受访问冲突及类似问题的影响(即使它不做任何事而只是传递参数)。该缓冲方法将由P/Invoke调用引入的任何潜在的错误本地化。

  将私有外部方法隐藏在公共包装后面的第三同时也是最后的一个好处是,提供了向该方法添加一些最小的CLR样式的机会。例如,在图1中,我将WindowsAPI函数返回的Boolean失败转换成更像CLR的异常。我还定义了一个名为BeepTypes的枚举类型,它的成员对应于同该WindowsAPI一起使用的定义值。由于C#不支持定义,因此可以使用托管枚举类型来避免幻数向整个应用程序代码扩散。

  包装方法的最后一个好处对于简单的WindowsAPI函数(如MessageBeep)诚然是微不足道的。但是当您开始调用更复杂的非托管函数时,您会发现,手动将WindowsAPI样式转换成对CLR更加友好的方法所带来的好处会越来越多。越是打算在整个应用程序中重用interop功能,越是应该认真地考虑包装的设计。同时我认为,在非面向对象的静态包装方法中使用对CLR友好的参数也并非不可以。

  DLLImport属性。

  现在是更深入地进行探讨的时候了。在对托管代码进行P/Invoke调用时,DllImportAttribute类型扮演着重要的角色。DllImportAttribute的主要作用是给CLR指示哪个DLL导出您想要调用的函数。相关DLL的名称被作为一个构造函数参数传递给DllImportAttribute。

  如果您无法肯定哪个DLL定义了您要使用的WindowsAPI函数,PlatformSDK文档将为您提供最好的帮助资源。在WindowsAPI函数主题文字临近结尾的位置,SDK文档指定了C应用程序要使用该函数必须链接的.lib文件。在几乎所有的情况下,该.lib文件具有与定义该函数的系统DLL文件相同的名称。例如,如果该函数需要C应用程序链接到Kernel32.lib,则该函数就定义在Kernel32.dll中。您可以在MessageBeep中找到有关MessageBeep的PlatformSDK文档主题。在该主题结尾处,您会注意到它指出库文件是User32.lib;这表明MessageBeep是从User32.dll中导出的。

  可选的DllImportAttribute属性。

  除了指出宿主DLL外,DllImportAttribute还包含了一些可选属性,其中四个特别有趣:EntryPoint、CharSet、SetLastError和CallingConvention。

  EntryPoint在不希望外部托管方法具有与DLL导出相同的名称的情况下,可以设置该属性来指示导出的DLL函数的入口点名称。当您定义两个调用相同非托管函数的外部方法时,这特别有用。另外,在Windows中还可以通过它们的序号值绑定到导出的DLL函数。如果您需要这样做,则诸如"#1"或"#129"的EntryPoint值指示DLL中非托管函数的序号值而不是函数名。CharSet对于字符集,并非所有版本的Windows都是同样创建的。Windows9x系列产品缺少重要的Unicode支持,而WindowsNT和WindowsCE系列则一开始就使用Unicode。在这些操作系统上运行的CLR将Unicode用于String和Char数据的内部表示。但也不必担心-当调用Windows9xAPI函数时,CLR会自动进行必要的转换,将其从Unicode转换为ANSI。如果DLL函数不以任何方式处理文本,则可以忽略DllImportAttribute的CharSet属性。然而,当Char或String数据是等式的一部分时,应该将CharSet属性设置为CharSet.Auto。这样可以使CLR根据宿主OS使用适当的字符集。如果没有显式地设置CharSet属性,则其默认值为CharSet.Ansi。这个默认值是有缺点的,因为对于在Windows2000、WindowsXP和WindowsNT上进行的interop调用,它会消极地影响文本参数封送处理的性能。

  应该显式地选择CharSet.Ansi或CharSet.Unicode的CharSet值而不是使用CharSet.Auto的唯一情况是:您显式地指定了一个导出函数,而该函数特定于这两种Win32OS中的某一种。ReadDirectoryChangesWAPI函数就是这样的一个例子,它只存在于基于WindowsNT的操作系统中,并且只支持Unicode;在这种情况下,您应该显式地使用CharSet.Unicode。

  有时,WindowsAPI是否有字符集关系并不明显。一种决不会有错的确认方法是在PlatformSDK中检查该函数的C语言头文件。(如果您无法肯定要看哪个头文件,则可以查看PlatformSDK文档中列出的每个API函数的头文件。)如果您发现该API函数确实定义为一个映射到以A或W结尾的函数名的宏,则字符集与您尝试调用的函数有关系。WindowsAPI函数的一个例子是在WinUser.h中声明的GetMessageAPI,您也许会惊讶地发现它有A和W两种版本。

  SetLastError错误处理非常重要,但在编程时经常被遗忘。当您进行P/Invoke调用时,也会面临其他的挑战-处理托管代码中WindowsAPI错误处理和异常之间的区别。我可以给您一点建议。

  如果您正在使用P/Invoke调用WindowsAPI函数,而对于该函数,您使用GetLastError来查找扩展的错误信息,则应该在外部方法的DllImportAttribute中将SetLastError属性设置为true。这适用于大多数外部方法。

  这会导致CLR在每次调用外部方法之后缓存由API函数设置的错误。然后,在包装方法中,可以通过调用类库的System.Runtime.InteropServices.Marshal类型中定义的Marshal.GetLastWin32Error方法来获取缓存的错误值。我的建议是检查这些期望来自API函数的错误值,并为这些值引发一个可感知的异常。对于其他所有失败情况(包括根本就没意料到的失败情况),则引发在System.ComponentModel命名空间中定义的Win32Exception,并将Marshal.GetLastWin32Error返回的值传递给它。如果您回头看一下图1中的代码,您会看到我在externMessageBeep方法的公共包装中就采用了这种方法。

  CallingConvention我将在此介绍的最后也可能是最不重要的一个DllImportAttribute属性是CallingConvention。通过此属性,可以给CLR指示应该将哪种函数调用约定用于堆栈中的参数。CallingConvention.Winapi的默认值是最好的选择,它在大多数情况下都可行。然而,如果该调用不起作用,则可以检查PlatformSDK中的声明头文件,看看您调用的API函数是否是一个不符合调用约定标准的异常API。

  通常,本机函数(例如WindowsAPI函数或C-运行时DLL函数)的调用约定描述了如何将参数推入线程堆栈或从线程堆栈中清除。大多数WindowsAPI函数都是首先将函数的最后一个参数推入堆栈,然后由被调用的函数负责清理该堆栈。相反,许多C-运行时DLL函数都被定义为按照方法参数在方法签名中出现的顺序将其推入堆栈,将堆栈清理工作交给调用者。

  幸运的是,要让P/Invoke调用工作只需要让*设备理解调用约定即可。通常,从默认值CallingConvention.Winapi开始是最好的选择。然后,在C运行时DLL函数和少数函数中,可能需要将约定更改为CallingConvention.Cdecl。

  数据封送处理。

  数据封送处理是P/Invoke具有挑战性的方面。当在托管和非托管代码之间传递数据时,CLR遵循许多规则,很少有开发人员会经常遇到它们直至可将这些规则记住。除非您是一名类库开发人员,否则在通常情况下没有必要掌握其细节。为了最有效地在CLR上使用P/Invoke,即使只偶尔需要interop的应用程序开发人员仍然应该理解数据封送处理的一些基础知识。

  在本月专栏的剩余部分中,我将讨论简单数字和字符串数据的数据封送处理。我将从最基本的数字数据封送处理开始,然后介绍简单的指针封送处理和字符串封送处理。

  封送数字和逻辑标量。

  WindowsOS大部分是用C编写的。因此,WindowsAPI所用到的数据类型要么是C类型,要么是通过类型定义或宏定义重新标记的C类型。让我们看看没有指针的数据封送处理。简单起见,首先重点讨论的是数字和布尔值。

  当通过值向WindowsAPI函数传递参数时,需要知道以下问题的*:数据从根本上讲是整型的还是浮点型的?如果数据是整型的,则它是有符号的还是无符号的?如果数据是整型的,则它的位数是多少?如果数据是浮点型的,则它是单精度的还是双精度的?有时*很明显,但有时却不明显。WindowsAPI以各种方式重新定义了基本的C数据类型。图2列出了C和Win32的一些公共数据类型及其规范,以及一个具有匹配规范的公共语言运行库类型。

  通常,只要您选择一个其规范与该参数的Win32类型相匹配的CLR类型,您的代码就能够正常工作。不过也有一些特例。例如,在WindowsAPI中定义的BOOL类型是一个有符号的32位整型。然而,BOOL用于指示Boolean值true或false。虽然您不用将BOOL参数作为System.Int32值封送,但是如果使用System.Boolean类型,就会获得更合适的映射。字符类型的映射类似于BOOL,因为有一个特定的CLR类型(System.Char)指出字符的含义。

  在了解这些信息之后,逐步介绍示例可能是有帮助的。依然采用beep主题作为例子,让我们来试一下Kernel32.dll低级Beep,它会通过计算机的扬声器发生嘟声。这个方法的PlatformSDK文档可以在Beep中找到。本机API按以下方式进行记录:BOOLBeep(DWORDdwFreq,//FrequencyDWORDdwDuration//Durationinmilliseconds);。

  在参数封送处理方面,您的工作是了解什么CLR数据类型与BeepAPI函数所使用的DWORD和BOOL数据类型相兼容。回顾一下图2中的图表,您将看到DWORD是一个32位的无符号整数值,如同CLR类型System.UInt32。这意味着您可以使用UInt32值作为送往Beep的两个参数。BOOL返回值是一个非常有趣的情况,因为该图表告诉我们,在Win32中,BOOL是一个32位的有符号整数。因此,您可以使用System.Int32值作为来自Beep的返回值。然而,CLR也定义了System.Boolean类型作为Boolean值的语义,所以应该使用它来替代。CLR默认将System.Boolean值封送为32位的有符号整数。此处所显示的外部方法定义是用于Beep的结果P/Invoke方法:[DllImport("Kernel32.dll",SetLastError=true)]staticexternBooleanBeep(UInt32frequency,UInt32duration);。

  指针参数。

  许多WindowsAPI函数将指针作为它们的一个或多个参数。指针增加了封送数据的复杂性,因为它们增加了一个间接层。如果没有指针,您可以通过值在线程堆栈中传递数据。有了指针,则可以通过引用传递数据,方法是将该数据的内存地址推入线程堆栈中。然后,函数通过内存地址间接访问数据。使用托管代码表示此附加间接层的方式有多种。

  在C#中,如果将方法参数定义为ref或out,则数据通过引用而不是通过值传递。即使您没有使用Interop也是这样,但只是从一个托管方法调用到另一个托管方法。例如,如果通过ref传递System.Int32参数,则在线程堆栈中传递的是该数据的地址,而不是整数值本身。下面是一个定义为通过引用接收整数值的方法的示例:voidFlipInt32(refInt32num){num=-num;}。

  这里,FlipInt32方法获取一个Int32值的地址、访问数据、对它求反,然后将求反过的值赋给原始变量。在以下代码中,FlipInt32方法会将调用程序的变量x的值从10更改为-10:Int32x=10;FlipInt32(refx);。

  在托管代码中可以重用这种能力,将指针传递给非托管代码。例如,FileEncryptionStatusAPI函数以32位无符号位掩码的形式返回文件加密状态。该API按以下所示方式进行记录:BOOLFileEncryptionStatus(LPCTSTRlpFileName,//filenameLPDWORDlpStatus//encryptionstatus);。

  请注意,该函数并不使用它的返回值返回状态,而是返回一个Boolean值,指示调用是否成功。在成功的情况下,实际的状态值是通过第二个参数返回的。它的工作方式是调用程序向该函数传递指向一个DWORD变量的指针,而该API函数用状态值填充指向的内存位置。以下代码片段显示了一个调用非托管FileEncryptionStatus函数的可能外部方法定义:[DllImport("Advapi32.dll",CharSet=CharSet.Auto)]staticexternBooleanFileEncryptionStatus(Stringfilename,outUInt32status);。

  该定义使用out关键字来为UInt32状态值指示by-ref参数。这里我也可以选择ref关键字,实际上在运行时会产生相同的机器码。out关键字只是一个by-ref参数的规范,它向C#编译器指示所传递的数据只在被调用的函数外部传递。相反,如果使用ref关键字,则编译器会假定数据可以在被调用的函数的内部和外部传递。

  托管代码中out和ref参数的另一个很好的方面是,地址作为by-ref参数传递的变量可以是线程堆栈中的一个本地变量、一个类或结构的元素,也可以是具有合适数据类型的数组中的一个元素引用。调用程序的这种灵活性使得by-ref参数成为封送缓冲区指针以及单数值指针的一个很好的起点。只有在我发现ref或out参数不符合我的需要的情况下,我才会考虑将指针封送为更复杂的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关键字,因为变量已经是对象的引用了。如果您对引用类型和值类型之间的差别不是很熟悉,请查阅2000年12月发行的MSDNMagazine,在.NET专栏的主题中可以找到更多信息。大多数CLR类型都是引用类型;然而,除了System.String和System.Object,所有的基元类型(例如System.Int32和System.Boolean)都是值类型。封送不透明(Opaque)指针:一种特殊情况。

  有时在WindowsAPI中,方法传递或返回的指针是不透明的,这意味着该指针值从技术角度讲是一个指针,但代码却不直接使用它。相反,代码将该指针返回给Windows以便随后进行重用。

  一个非常常见的例子就是句柄的概念。在Windows中,内部数据结构(从文件到屏幕上的按钮)在应用程序代码中都表示为句柄。句柄其实就是不透明的指针或有着指针宽度的数值,应用程序用它来表示内部的OS构造。

  少数情况下,API函数也将不透明指针定义为PVOID或LPVOID类型。在WindowsAPI的定义中,这些类型意思就是说该指针没有类型。

  当一个不透明指针返回给您的应用程序(或者您的应用程序期望得到一个不透明指针)时,您应该将参数或返回值封送为CLR中的一种特殊类型-System.IntPtr。当您使用IntPtr类型时,通常不使用out或ref参数,因为IntPtr意为直接持有指针。不过,如果您将一个指针封送为一个指针,则对IntPtr使用by-ref参数是合适的。

  在CLR类型系统中,System.IntPtr类型有一个特殊的属性。不像系统中的其他基类型,IntPtr并没有固定的大小。相反,它在运行时的大小是依底层操作系统的正常指针大小而定的。这意味着在32位的Windows中,IntPtr变量的宽度是32位的,而在64位的Windows中,实时编译器编译的代码会将IntPtr值看作64位的值。当在托管代码和非托管代码之间封送不透明指针时,这种自动调节大小的特点十分有用。

  请记住,任何返回或接受句柄的API函数其实操作的就是不透明指针。您的代码应该将Windows中的句柄封送成System.IntPtr值。

  您可以在托管代码中将IntPtr值强制转换为32位或64位的整数值,或将后者强制转换为前者。然而,当使用WindowsAPI函数时,因为指针应是不透明的,所以除了存储和传递给外部方法外,不能将它们另做它用。这种"只限存储和传递"规则的两个特例是当您需向外部方法传递null指针值和需要比较IntPtr值与null值的情况。为了做到这一点,您不能将零强制转换为System.IntPtr,而应该在IntPtr类型上使用Int32.Zero静态公共字段,以便获得用于比较或赋值的null值。

  封送文本。

  在编程时经常要对文本数据进行处理。文本为interop制造了一些麻烦,这有两个原因。首先,底层操作系统可能使用Unicode来表示字符串,也可能使用ANSI。在极少数情况下,例如MultiByteToWideCharAPI函数的两个参数在字符集上是不一致的。

  第二个原因是,当需要进行P/Invoke时,要处理文本还需要特别了解到C和CLR处理文本的方式是不同的。在C中,字符串实际上只是一个字符值数组,通常以null作为结束符。大多数WindowsAPI函数是按照以下条件处理字符串的:对于ANSI,将其作为字符值数组;对于Unicode,将其作为宽字符值数组。

  幸运的是,CLR被设计得相当灵活,当封送文本时问题得以轻松解决,而不用在意WindowsAPI函数期望从您的应用程序得到的是什么。这里是一些需要记住的主要考虑事项:是您的应用程序向API函数传递文本数据,还是API函数向您的应用程序返回字符串数据?或者二者兼有?您的外部方法应该使用什么托管类型?API函数期望得到的是什么格式的非托管字符串?我们首先解答最后一个问题。大多数WindowsAPI函数都带有LPTSTR或LPCTSTR值。(从函数角度看)它们分别是可修改和不可修改的缓冲区,包含以null结束的字符数组。"C"代表常数,意味着使用该参数信息不会传递到函数外部。LPTSTR中的"T"表明该参数可以是Unicode或ANSI,取决于您选择的字符集和底层操作系统的字符集。因为在WindowsAPI中大多数字符串参数都是这两种类型之一,所以只要在DllImportAttribute中选择CharSet.Auto,CLR就按默认的方式工作。

  然而,有些API函数或自定义的DLL函数采用不同的方式表示字符串。如果您要用到一个这样的函数,就可以采用MarshalAsAttribute修饰外部方法的字符串参数,并指明一种不同于默认LPTSTR的字符串格式。有关MarshalAsAttribute的更多信息,请参阅位于MarshalAsAttributeClass的PlatformSDK文档主题。

  现在让我们看一下字符串信息在您的代码和非托管函数之间传递的方向。有两种方式可以知道处理字符串时信息的传递方向。第一个也是最可靠的一个方法就是首先理解参数的用途。例如,您正调用一个参数,它的名称类似CreateMutex并带有一个字符串,则可以想像该字符串信息是从应用程序向API函数传递的。同时,如果您调用GetUserName,则该函数的名称表明字符串信息是从该函数向您的应用程序传递的。

  除了这种比较合理的方法外,第二种查找信息传递方向的方式就是查找API参数类型中的字母"C"。例如,GetUserNameAPI函数的第一个参数被定义为LPTSTR类型,它代表一个指向Unicode或ANSI字符串缓冲区的长指针。但是CreateMutex的名称参数被类型化为LTCTSTR。请注意,这里的类型定义是一样的,但增加一个字母"C"来表明缓冲区为常数,API函数不能写入。

  一旦明确了文本参数是只用作输入还是用作输入/输出,就可以确定使用哪种CLR类型作为参数类型。这里有一些规则。如果字符串参数只用作输入,则使用System.String类型。在托管代码中,字符串是不变的,适合用于不会被本机API函数更改的缓冲区。

  如果字符串参数可以用作输入和/或输出,则使用System.StringBuilder类型。StringBuilder类型是一个很有用的类库类型,它可以帮助您有效地构建字符串,也正好可以将缓冲区传递给本机函数,由本机函数为您填充字符串数据。一旦函数调用返回,您只需要调用StringBuilder对象的ToString就可以得到一个String对象。

  GetShortPathNameAPI函数能很好地用于显示什么时候使用String、什么时候使用StringBuilder,因为它只带有三个参数:一个输入字符串、一个输出字符串和一个指明输出缓冲区的字符长度的参数。

  图3所示为加注释的非托管GetShortPathName函数文档,它同时指出了输入和输出字符串参数。它引出了托管的外部方法定义,也如图3所示。请注意第一个参数被封送为System.String,因为它是一个只用作输入的参数。第二个参数代表一个输出缓冲区,它使用了System.StringBuilder。

  小结。

  本月专栏所介绍的P/Invoke功能足够调用Windows中的许多API函数。然而,如果您大量用到interop,则会最终发现自己封送了很复杂的数据结构,甚至可能需要在托管代码中通过指针直接访问内存。实际上,本机代码中的interop可以是一个将细节和低级比特藏在里面的真正的潘多拉盒子。CLR、C#和托管C++提供了许多有用的功能;也许以后我会在本专栏介绍高级的P/Invoke话题。

  同时,只要您觉得.NETFramework类库无法播放您的声音或者为您执行其他一些功能,您可以知道如何向原始而优秀的WindowsAPI寻求一些帮助。

原创粉丝点击