引言
DllImportAttribute
属性提供调用非托管函数的规范。在对托管代码进行P/Invoke
调用时,DllImportAttribute
类型扮演着重要的角色。
DllImportAttribute
的主要作用就是给CLR
指示哪个DLL
导出您想要的调用的函数。相关DLL
的名称被作为一个构造函数参数传递给DllImportAttribute
。
常用属性介绍
EntryPoint
指定要调用的
DLL
入口点的名称或序号(默认入口点名称就是托管方法的名称)。序号以“#”符号为前缀,如#1。如果省略此字段,则
CLR
将使用以DllImportAttribute
标记的.NET
方法的名称。
EntryPoint
在不希望外部托管方法具有与DLL
导出相同的名称的情况下,可以设置该属性来指示导出的DLL
函数的入口点名称。当定义两个调用相同非托管函数的外部方法时,这特别有用。另外,在
Windows
中还可以通过它们的序号值绑定到导出的DLL
函数。如果需要这样做,则诸如
#1
或#129
的EntryPoint
值指示DLL
中非托管函数的序号值而不是函数名。示意代码:
using System; using System.Runtime.InteropServices; class Example { // Use DllImport to import the Win32 MessageBox function. // Specify the method to import using the EntryPoint field and // then change the name to MyNewMessageBoxMethod. [DllImport("user32.dll", CharSet = CharSet.Auto, EntryPoint = "MessageBox")] public static extern int MyNewMessageBoxMethod(IntPtr hWnd, String text, String caption, uint type); static void Main() { // Call the MessageBox function using platform invoke. MyNewMessageBoxMethod(new IntPtr(0), "Hello World!", "Hello Dialog", 0); } }
CharSet
指示如何向方法封送字符串参数。并控制名称重整。
通过一个
CharSet
枚举的成员使用此字段指定字符串参数的封送处理行为,并指定要调用的入口点名称。用于
C#
和Visual Basic
的默认枚举成员为CharSet.Ansi
,用于C++
的默认枚举成员为CharSet.None
,它与CharSet.Ansi
等效。在
Visual Basic
中可以使用Declare
语句指定CharSet
字段。
ExactSpelling
字段会影响CharSet
字段在确定要调用的入口点名称时的行为。
CharSet
对于字符集,并非所有版本的Windows
都是同样创建的。Windows 9x
系列产品缺少重要的Unicode
支持,而Windows NT
和Windows CE
系列则一开始就使用Unicode
。在这些操作系统上运行的CLR
将Unicode
用于String
和Char
数据的内部表示。但也不必担心 — 当调用Windows 9x API
函数时,CLR
会自动进行必要的转换,将其从Unicode
转换为ANSI
。如果
DLL
函数不以任何方式处理文本,则可以忽略DllImportAttribute
的CharSet
属性。然而,当Char
或String
数据是等式的一部分时,应该将CharSet
属性设置为CharSet.Auto
。这样可以使CLR
根据宿主OS
使用适当的字符集。
如果没有显式地设置CharSet
属性,则其默认值为CharSet.Ansi
。这个默认值是有缺点的,因为对于在Windows 2000
、Windows XP
和Windows NT®
上进行的interop
调用,它会消极地影响文本参数封送处理的性能。应该显式地选择 CharSet.Ansi 或 CharSet.Unicode 的 CharSet 值而不是使用 CharSet.Auto 的唯一情况是:您显式地指定了一个导出函数,而该函数特定于这两种 Win32 OS 中的某一种。ReadDirectoryChangesW API 函数就是这样的一个例子,它只存在于基于 Windows NT 的操作系统中,并且只支持 Unicode;在这种情况下,您应该显式地使用 CharSet.Unicode。
有时,
Windows API
是否有字符集关系并不明显。一种决不会有错的确认方法是在Platform SDK
中检查该函数的C
语言头文件。示意代码:
using System; using System.Runtime.InteropServices; class Example { // Use DllImport to import the Win32 MessageBox function. // Specify the method to import using the EntryPoint field and // then change the name to MyNewMessageBoxMethod. [DllImport("user32.dll", CharSet = CharSet.Auto, EntryPoint = "MessageBox")] public static extern int MyNewMessageBoxMethod(IntPtr hWnd, String text, String caption, uint type); static void Main() { // Call the MessageBox function using platform invoke. MyNewMessageBoxMethod(new IntPtr(0), "Hello World!", "Hello Dialog", 0); } }
SetLastError
用来指示被调用方在从属性化方法返回之前是否调用
SetLastError Win32 API
函数。true
指示被调用方将调用SetLastError
;否则为false
。默认值为
false
。运行时封送拆收器将调用GetLastError
并缓存返回的值,以防其被其他API
调用覆盖。
SetLastError
错误处理非常重要,但在编程时经常被遗忘。当您进行P/Invoke
调用时,也会面临其他的挑战 — 处理托管代码中Windows API
错误处理和异常之间的区别。经常用到的一个解决方案是:
如果正在使用
P/Invoke
调用Windows API
函数,而对于该函数,使用GetLastError
来查找扩展的错误信息,则应该在外部方法的DllImportAttribute
中将SetLastError
属性设置为true
。这适用于大多数外部方法。这会导致CLR
在每次调用外部方法之后缓存由API
函数设置的错误。然后,在包装方法中,可以通过调用类库的System.Runtime.InteropServices.Marshal
类型中定义的Marshal.GetLastWin32Error
方法来获取缓存的错误值。
并且检查这些期望来自API
函数的错误值,并为这些值引发一个可感知的异常。对于其他所有失败情况(包括根本就没意料到的失败情况),则引发在System.ComponentModel
命名空间中定义的Win32Exception
,并将Marshal.GetLastWin32Error
返回的值传递给它。示意代码:
using System.Runtime.InteropServices; public class Win32 { [DllImport("user32.dll",SetLastError=true)] public static extern int MessageBoxA(int hWnd,String text,String caption,uint type); }
CallingConvention
指定传递方法参数时使用的调用约定值。默认值为
CallingConvention.Winapi
(Winapi
默认StdCall
约定),它在大多数情况下都可行。该值与Windows CE
平台上的__cdecl
相对应。然而,如果该调用不起作用,则可以检查
Platform SDK
中的声明头文件,看看调用的API
函数是否是一个不符合调用约定标准的异常API
。通常,本机函数(例如
Windows API
函数或C- 运行时 DLL
函数)的调用约定描述了如何将参数推入线程堆栈或从线程堆栈中清除。
大多数
Windows API
函数都是首先将函数的最后一个参数推入堆栈,然后由被调用的函数负责清理该堆栈。相反,许多
C-运行时 DLL
函数都被定义为按照方法参数在方法签名中出现的顺序将其推入堆栈,将堆栈清理工作交给调用者。幸运的是,要让
P/Invoke
调用工作只需要让外围设备理解调用约定即可。通常,从默认值CallingConvention.Winapi
开始是最好的选择。然后,在C 运行时 DLL
函数和少数函数中,可能需要将约定更改为CallingConvention.Cdecl
。示意代码:
using System; using System.Runtime.InteropServices; public class LibWrap { // C# doesn't support varargs so all arguments must be explicitly defined. // CallingConvention.Cdecl must be used since the stack is // cleaned up by the caller. // int printf(const char *format [, argument]...) [DllImport("msvcrt.dll", CharSet=CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)] public static extern int printf(String format, int i, double d); [DllImport("msvcrt.dll", CharSet=CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)] public static extern int printf(String format, int i, String s); } public class App { public static void Main() { LibWrap.printf("\nPrint params: %i %f", 99, 99.99); LibWrap.printf("\nPrint params: %i %s", 99, "abcd"); } }