2

我在 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)' 的可访问性,使其不再从其程序集外部可见。

好的。将可访问性更改为 internal 为:

[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)' 由于其保护级别而无法访问

那么我怎样才能在没有错误或警告的情况下完成这项工作呢?

提前感谢您的帮助,因为我已经转了几个小时!

4

1 回答 1

16

可以肯定的是,您会同意 PInvoke 方法不是从 C# 代码中调用的最愉快的事情的说法。

他们是:

  1. 不是那么强类型 - 经常充斥着IntPtrByte[]参数。
  2. 容易出错 - 很容易传递一些未正确初始化的参数,例如长度错误的缓冲区,或者某些字段未初始化为该结构大小的结构......
  3. 显然,如果出现问题,不要抛出异常——检查返回码或Marshal.GetLastError()它是消费者的责任。更常见的是,有人忘记了这样做,从而导致难以跟踪的错误。

与这些问题相比,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. 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# 枚举实际上也不过是整数,因此假设您设置了正确的底层类型,您将拥有更容易使用的方法。

  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;
        }
    }
    

PS:不要忘记正确的函数签名(位数和其他问题)、类型的编组、布局属性和字符集(另外,不要忘记使用DllImport(... SetLastError = true)最重要的)。http://www.pinvoke.net/可能经常有帮助,但它并不总是提供最好的签名来使用。

PS1:我建议你NativeMethods不要将你的类组织成一个类,因为它很快就会变成一大堆完全不同的方法,而是将它们分组到单独的类中(我实际上使用一个partial根类和每个功能区域的嵌套类- 更多乏味的打字,但更好的上下文和智能感知)。对于类名,我只使用 MSDN 用于对 API 函数进行分组的相同分类。与GetSystemInfo一样,它是“系统信息功能”


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

于 2016-03-05T20:35:49.633 回答