P/Invoke是什么?
在受控代码与非受控代码进行交互时会产生一个事务(transition) ,这通常发生在使用平台调用服务(Platform Invocation Services),即P/Invoke
如调用系统的 API 或与 COM 对象打交道,通过 System.Runtime.InteropServices 命名空间
虽然使用 Interop 非常方便,但据估计每次调用事务都要执行 10 到 40 条指令,算起来开销也不少,所以我们要尽量少调用事务
如果非用不可,建议本着一次调用执行多个动作,而不是多次调用每次只执行少量动作的原则。
在对托管代码进行 P/Invoke 调用时,DllImportAttribute 类型扮演着重要的角色。DllImportAttribute 的主要作用是给 CLR 指示哪个 DLL 导出您想要调用的函数。相关 DLL 的名称被作为一个构造函数参数传递给 DllImportAttribute。下面是使用P/Invoke时的一个简单例子:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
1,"KERNEL32.DLL", 为必选属性,用来指出相关 DLL 的名称,The name of the DLL that contains the unmanaged method.
2,EntryPoint:指示要调用的 DLL 入口点的名称或序号.
指定入口点名称时,您可以提供一个字符串来指示包含入口点的 DLL 的名称,或者也可以按序号来标识入口点。序号以 # 符号为前缀,如 #1。如果省略此字段,则公共语言运行库将使用以 DllImportAttribute 标记的 .NET 方法的名称。
在不希望外部托管方法具有与 DLL 导出相同的名称的情况下,可以设置该属性来指示导出的 DLL 函数的入口点名称。当您定义两个调用相同非托管函数的外部方法时,这特别有用。
3,CharSet 指示如何向方法封送字符串参数,并控制名称损坏。
通过一个 CharSet 枚举的成员使用此字段指定字符串参数的封送处理行为,并指定要调用的入口点名称(给定的确切名称或以“A”、“W”结尾的名称)。用于 C# 和 Visual Basic 的默认枚举成员为 CharSet.Ansi,用于 C++ 的默认枚举成员为 CharSet.None,它与 CharSet.Ansi 等效。在 Visual Basic 中可以使用 Declare 语句指定 CharSet 字段。
ExactSpelling字段会影响 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 语言头文件。(如果您无法肯定要看哪个头文件,则可以查看 Platform SDK 文档中列出的每个 API 函数的头文件。)如果您发现该 API 函数确实定义为一个映射到以 A 或 W 结尾的函数名的宏,则字符集与您尝试调用的函数有关系。Windows API 函数的一个例子是在 WinUser.h 中声明的 GetMessage API,您也许会惊讶地发现它有 A 和 W 两种版本。
4,SetLastError 指示被调用方在从属性化方法返回之前是否调用 SetLastError Win32 API 函数。
true 指示被调用方将调用 SetLastError;否则为 false。默认值为 false,但在 Visual Basic 中除外。
运行时封送拆收器将调用 GetLastError 并缓存返回的值,以防其被其他 API 调用重写。可通过调用 GetLastWin32Error来检索错误代码。
错误处理非常重要,但在编程时经常被遗忘。当您进行 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 返回的值传递给它。看下面的例子:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
5,CallingConvention 指示入口点的调用约定。
将此字段设置为 CallingConvention 枚举成员之一。CallingConvention 字段的默认值为 WinAPI,而后者又默认为 StdCall 约定。
我将在此介绍的最后也可能是最不重要的一个 DllImportAttribute 属性是 CallingConvention。通过此属性,可以给 CLR 指示应该将哪种函数调用约定用于堆栈中的参数。CallingConvention.Winapi 的默认值是最好的选择,它在大多数情况下都可行。然而,如果该调用不起作用,则可以检查 Platform SDK 中的声明头文件,看看您调用的 API 函数是否是一个不符合调用约定标准的异常 API。
通常,本机函数(例如 Windows API 函数或 C- 运行时 DLL 函数)的调用约定描述了如何将参数推入线程堆栈或从线程堆栈中清除。大多数 Windows API 函数都是首先将函数的最后一个参数推入堆栈,然后由被调用的函数负责清理该堆栈。相反,许多 C-运行时 DLL 函数都被定义为按照方法参数在方法签名中出现的顺序将其推入堆栈,将堆栈清理工作交给调用者。
幸运的是,要让 P/Invoke 调用工作只需要让外围设备理解调用约定即可。通常,从默认值 CallingConvention.Winapi 开始是最好的选择。然后,在 C 运行时 DLL 函数和少数函数中,可能需要将约定更改为 CallingConvention.Cdecl。
6,ExactSpelling:控制 DllImportAttribute.CharSet 字段是否使公共语言运行库在非托管 DLL 中搜索入口点名称,而不使用指定的入口点名称。
如果为 false,则当 DllImportAttribute.CharSet 字段设置为 CharSet.Ansi 时,将调用附加有字母“A”的入口点名称;当 DllImportAttribute.CharSet 字段设置为 CharSet.Unicode 时,将调用附加有字母“W”的入口点名称。此字段通常由托管编译器设置。下表根据编程语言设置的默认值,说明了 CharSet 字段和 ExactSpelling 字段之间的关系。您可以重写默认设置,但须谨慎。
Language | ANSI | Unicode | Auto |
---|---|---|---|
Visual Basic | ExactSpelling:=True | ExactSpelling:=True | ExactSpelling:=False |
C# | ExactSpelling=false | ExactSpelling=false | ExactSpelling=false |
C++ | ExactSpelling=false | ExactSpelling=false | ExactSpelling=false |
详细看下面的例子:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)