如何正确调用类库中的P / Invoke方法?

时间:2016-03-05 19:13:52

标签: c# pinvoke native-methods

我在Visual Studio 2015解决方案中有多个项目。其中一些项目执行P / Invokes,如:

 [DllImport("IpHlpApi.dll")]
        [return: MarshalAs(UnmanagedType.U4)]
        public static extern int GetIpNetTable(IntPtr pIpNetTable, [MarshalAs(UnmanagedType.U4)]
        ref int pdwSize, bool bOrder);

因此,我将所有P / Invokes移动到一个单独的类库中,并将单个类定义为:

namespace NativeMethods
{
    [
    SuppressUnmanagedCodeSecurityAttribute(),
    ComVisible(false)
    ]

    public static class SafeNativeMethods
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        public static extern int GetTickCount();

        // Declare the GetIpNetTable function.
        [DllImport("IpHlpApi.dll")]
        [return: MarshalAs(UnmanagedType.U4)]
        public static extern int GetIpNetTable(IntPtr pIpNetTable, [MarshalAs(UnmanagedType.U4)]
        ref int pdwSize, bool bOrder);
    }
}

从其他项目中,此代码称为:

 int result = SafeNativeMethods.GetIpNetTable(IntPtr.Zero, ref bytesNeeded, false);

所有编译都没有错误或警告。

现在在代码上运行FxCop会发出警告:

  

警告CA1401更改P / Invoke的可访问性   'SafeNativeMethods.GetIpNetTable(IntPtr,ref int,bool)'因此它是   从组装外部看不到。

确定。将可访问性更改为内部:

[DllImport("IpHlpApi.dll")]
[return: MarshalAs(UnmanagedType.U4)]
internal static extern int GetIpNetTable(IntPtr pIpNetTable, [MarshalAs(UnmanagedType.U4)]
ref int pdwSize, bool bOrder);

现在导致以下硬错误:

  

错误CS0122'SafeNativeMethods.GetIpNetTable(IntPtr,ref int,bool)'   由于其保护级别而无法访问

那么如何在没有错误或警告的情况下完成这项工作?

提前感谢您提供任何帮助,因为我已经圈了好几个小时了!

1 个答案:

答案 0 :(得分:10)

您肯定会同意PInvoke方法不是从C#代码调用的最令人愉快的事情。

他们是:

  1. 没有那么强烈的输入 - 经常充斥着IntPtrByte[]参数。
  2. 容易出错 - 很容易传递一些错误的初始化参数,比如一个长度错误的缓冲区,或者一些没有初始化为该结构大小的字段的结构......
  3. 显然,如果出现问题,不要抛出异常 - 消费者有责任检查返回代码或Marshal.GetLastError()。而且更常见的是,有人忘了这样做,导致难以追踪的错误。
  4. 与这些问题相比,FxCop警告只是一种微薄的风格检查。

    那么,你能做什么?处理这三个问题,FxCop将自行解决。

    这些是我建议你做的事情:

    1. 不要直接暴露任何API。这对于复杂的功能很重要,但将其应用于任何功能实际上都会处理您的主要FxCop问题:

      public static class ErrorHandling
      {
          // It is private so no FxCop should trouble you
          [DllImport(DllNames.Kernel32)]
          private static extern void SetLastErrorNative(UInt32 dwErrCode);
      
          public static void SetLastError(Int32 errorCode)
          {
              SetLastErrorNative(unchecked((UInt32)errorCode));
          }
      }
      
    2. 如果您可以使用safe handle,请不要使用IntPtr

    3. 不要只从包装器方法返回Boolean(U)Int32 - 检查包装器方法中的返回类型,并在需要时抛出异常。如果你想以无异常的方式使用方法,那么提供Try - 就像明确表示它是一种无异常方法的版本一样。

      public static class Window
      {
          public class WindowHandle : SafeHandle ...
      
          [return: MarshalAs(UnmanagedType.Bool)]
          [DllImport(DllNames.User32, EntryPoint="SetForegroundWindow")]
          private static extern Boolean TrySetForegroundWindowNative(WindowHandle hWnd);
      
          // It is clear for everyone, that the return value should be checked.
          public static Boolean TrySetForegroundWindow(WindowHandle hWnd)
          {
              if (hWnd == null)
                  throw new ArgumentNullException(paramName: nameof(hWnd));
      
              return TrySetForegroundWindowNative(hWnd);
          }
      
          public static void SetForegroundWindow(WindowHandle hWnd)
          {
              if (hWnd == null)
                  throw new ArgumentNullException(paramName: nameof(hWnd));
      
              var isSet = TrySetForegroundWindow(hWnd);
              if (!isSet)
                  throw new InvalidOperationException(
                      String.Format(
                          "Failed to set foreground window {0}", 
                          hWnd.DangerousGetHandle());
          }
      }
      
    4. 如果您可以使用IntPtr 传递的普通结构,请不要使用Byte[]ref/out。您可能会说这很明显,但在很多情况下可以传递强类型结构,我已经看到IntPtr被使用了。不要在面向公众的方法中使用out参数。在大多数情况下,这是不必要的 - 您只需返回值。

      public static class SystemInformation
      {
          public struct SYSTEM_INFO { ... };
      
          [DllImport(DllNames.Kernel32, EntryPoint="GetSystemInfo")]
          private static extern GetSystemInfoNative(out SYSTEM_INFO lpSystemInfo);
      
          public static SYSTEM_INFO GetSystemInfo()
          {
              SYSTEM_INFO info;
              GetSystemInfoNative(out info);
              return info;
          }
      }
      
    5. 枚举。 WinApi使用大量枚举值作为参数或返回值。作为C风格的枚举,它们实际上是作为简单整数传递(返回)的。但是C#枚举实际上只不过是整数,所以假设你有set proper underlying type,你将更容易使用方法。

    6. Bit / Byte twiddling - 每当您看到获取某些值或检查其正确性需要一些掩码时,您就可以确定使用自定义包装器可以更好地处理它。有时用FieldOffset来处理它,有时候应该做一些实际的比特,但无论如何它只能在一个地方完成,提供简单而方便的对象模型:

      public static class KeyBoardInput
      {
          public enum VmKeyScanState : byte
          {
              SHIFT = 1,
              CTRL = 2, ...
          }           
      
          public enum VirtualKeyCode : byte
          {
              ...
          }
      
          [StructLayout(LayoutKind.Explicit)]
          public struct VmKeyScanResult
          {
              [FieldOffset(0)]
              private VirtualKeyCode _virtualKey;
              [FieldOffset(1)]
              private VmKeyScanState _scanState;
      
              public VirtualKeyCode VirtualKey
              {
                  get {return this._virtualKey}
              }
              public VmKeyScanState ScanState
              {
                  get {return this._scanState;}
              }
      
              public Boolean IsFailure
              {
                  get
                  {
                      return 
                          (this._scanState == 0xFF) &&
                          (this._virtualKey == 0xFF)
                  }                   
              }
          }
      
      
          [DllImport(DllNames.User32, CharSet=CharSet.Unicode, EntryPoint="VmKeyScan")]
          private static extern VmKeyScanResult VmKeyScanNative(Char ch);
      
          public static VmKeyScanResult TryVmKeyScan(Char ch)
          {
              return VmKeyScanNative(ch);
          }
      
          public static VmKeyScanResult VmKeyScan(Char ch)
          {
              var result = VmKeyScanNative(ch);   
              if (result.IsFailure)
                  throw new InvalidOperationException(
                      String.Format(
                          "Failed to VmKeyScan the '{0}' char",
                          ch));
              return result;
          }
      }
      
    7. PS:并且不要忘记正确的功能签名(位数和其他问题),类型的编组,布局属性和字符集(同样,不要忘记使用DllImport(... SetLastError = true)utmost importance)。 http://www.pinvoke.net/可能经常有所帮助,但它并不总能提供最好的签名。

      PS1:我建议你将NativeMethods组织成一个班级,因为它很快就会成为一个非常难以管理的一堆完全不同的方法,而是将它们组合成一个单独的类(我实际上使用一个partial根类和每个功能区的嵌套类 - 更多的单调乏味,但更好的上下文和智能感知。对于类名,我只使用MSDN用于分组API函数的相同分类。与GetSystemInfo类似,它是“系统信息功能”

      因此,如果您应用所有这些建议,您将能够创建一个健壮,易于使用的本机包装库,隐藏所有不必要的复杂性和容易出错的构造,但对于任何知道这一点的人来说,这看起来都非常熟悉原始API。