P/Invoke是什么?

本文深入探讨了P/Invoke的工作原理及使用技巧,介绍了如何通过DllImportAttribute实现受控代码与非受控代码间的交互,包括正确设置CharSet、CallingConvention等属性以确保高效稳定的互操作。

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时的一个简单例子:

Code
[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW",  SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.StdCall)]
public static extern bool MoveFile(String src, String dst);

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 返回的值传递给它。看下面的例子:


Code
namespace Wintellect.Interop.Sound{
   using System;
   using System.Runtime.InteropServices;
   using System.ComponentModel;

   sealed class Sound{
      public static void MessageBeep(BeepTypes type){
         if(!MessageBeep((UInt32) type)){
            Int32 err = Marshal.GetLastWin32Error();
            throw new Win32Exception(err);
         }
      }

      [DllImport("User32.dll", SetLastError=true)]
      static extern Boolean MessageBeep(UInt32 beepType);

      private Sound(){}
   }

   enum BeepTypes{
      Simple = -1,
      Ok                = 0x00000000,
      IconHand          = 0x00000010,
      IconQuestion      = 0x00000020,
      IconExclamation   = 0x00000030,
      IconAsterisk      = 0x00000040
   }
}
 

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

 

详细看下面的例子:


Code
using System.Runtime.InteropServices;
public class Win32 {
    [DllImport("user32.dll", CharSet=CharSet.Unicode,
               ExactSpelling=true)]
    public static extern int MessageBoxW(int hWnd, String text, String
                                          caption, uint type);
}

 

### 调试 C# 中 P/Invoke 调用问题的方法 调试 C# 中的 P/Invoke 调用问题,尤其是与 `visa32.dll` 相关的问题,需要从多个角度入手。以下是一些专业且完整的调试方法: #### 1. 确保函数签名正确 P/Invoke 的核心是定义正确的函数签名。如果签名不匹配,可能会导致运行时错误或不可预期的行为。以下是需要注意的关键点: - **调用约定**:确保使用正确的调用约定(如 `CallingConvention.Cdecl` 或 `CallingConvention.StdCall`)。对于 `visa32.dll`,通常使用 `CallingConvention.Cdecl`[^1]。 - **参数类型**:确保参数类型与目标 DLL 的函数原型一致。例如,字符串参数可能需要指定为 `CharSet.Ansi` 或 `CharSet.Unicode`,具体取决于目标函数的要求[^1]。 - **返回值类型**:明确返回值类型,并根据返回值判断调用是否成功。 ```csharp [DllImport("VISA32.DLL", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern int viOpenDefaultRM(out IntPtr session); ``` #### 2. 捕获和处理错误码 `visa32.dll` 函数通常会返回一个整数值作为错误码。通过检查这些错误码,可以定位问题所在。例如,`viOpenDefaultRM` 返回非零值时表示操作失败[^1]。 ```csharp int result = viOpenDefaultRM(out IntPtr session); if (result != 0) { Console.WriteLine($"打开默认资源管理器失败,错误码: {result}"); } ``` #### 3. 使用工具分析调用 - **Dependency Walker**:可以用来查看 `visa32.dll` 的导出函数及其签名,帮助验证 P/Invoke 定义是否正确。 - **Process Monitor**:监控程序运行时对 `visa32.dll` 的加载和调用情况,定位潜在问题。 #### 4. 验证环境配置 确保系统中已正确安装 NI-VISA 驱动程序,并且 `visa32.dll` 文件路径已被添加到系统的 PATH 环境变量中。如果未正确配置,可能会导致 `DllNotFoundException`。 #### 5. 日志记录和异常处理 在代码中加入详细的日志记录,尤其是在关键步骤(如打开资源、读写数据)后记录返回值或状态信息。此外,使用 `try-catch` 块捕获异常并输出相关信息。 ```csharp try { int result = viFindRsrc(defaultRM, "?*", out instr, out uint returnCount, resourceDescription); if (result != 0) { Console.WriteLine($"查找资源失败,错误码: {result}"); } } catch (Exception ex) { Console.WriteLine($"发生异常: {ex.Message}"); } ``` #### 6. 测试简单场景 在复杂场景下调试可能会增加难度。建议先测试简单的功能,例如打开默认资源管理器或列出可用资源,确保基础功能正常后再逐步扩展到更复杂的交互。 #### 7. 参考官方示例 NI-VISA 提供了丰富的示例代码,可以帮助开发者更好地理解 API 的使用方式。例如,按默认路径安装后,可以在 `C:\Users\Public\Documents\National Instruments\NI-VISA\Examples.NET\17.5\SimpleReadWrite` 中找到官方范例代码[^2]。 #### 8. 检查目标平台 确保目标平台(如 x86 或 x64)与 `visa32.dll` 的版本匹配。如果目标平台不一致,可能会导致调用失败。 --- ### 示例代码:调试 P/Invoke 调用 以下是一个完整的调试示例,展示了如何结合日志记录和错误处理来排查问题。 ```csharp using System; using System.Runtime.InteropServices; class Visa32DebugExample { [DllImport("VISA32.DLL", EntryPoint = "#271", ExactSpelling = true, CharSet = CharSet.Ansi, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int viOpenDefaultRM(out IntPtr session); [DllImport("VISA32.DLL", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern int viClose(IntPtr vi); static void Main(string[] args) { IntPtr defaultRM = IntPtr.Zero; try { int result = viOpenDefaultRM(out defaultRM); if (result != 0) { Console.WriteLine($"打开默认资源管理器失败,错误码: {result}"); return; } Console.WriteLine("默认资源管理器已成功打开。"); } catch (Exception ex) { Console.WriteLine($"发生异常: {ex.Message}"); } finally { if (defaultRM != IntPtr.Zero) { viClose(defaultRM); Console.WriteLine("资源管理器已关闭。"); } } } } ``` --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值