如何在 class 库中正确调用 P/Invoke 方法?

How to correctly call P/Invoke methods in a class library?

我在 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 移动到一个单独的 class 库并将单个 class 定义为:

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给出了警告:

Warning CA1401 Change the accessibility of P/Invoke 'SafeNativeMethods.GetIpNetTable(IntPtr, ref int, bool)' so that it is no longer visible from outside its assembly.

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

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

现在导致硬错误:

Error CS0122 'SafeNativeMethods.GetIpNetTable(IntPtr, ref int, bool)' is inaccessible due to its protection level

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

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

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

他们是:

  1. 类型不那么强 - 通常充斥着 IntPtrByte[] 参数。
  2. 容易出错 - 很容易传递一些未正确初始化的参数,例如长度错误的缓冲区,或者某些字段未初始化为该结构大小的结构...
  3. 显然,如果出现问题,不要抛出异常 - 他们的消费者有责任检查 return 代码或 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. 如果可以使用一些safe handle就不要使用IntPtr

  3. 不要只是 return Boolean(U)Int32 来自包装器方法 - 检查 return 在包装器方法中键入并在需要时抛出异常。如果您想以无异常的方式使用方法,请提供类似 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. 如果可以使用 ref/out 传递的普通结构,请不要使用 IntPtrByte[]。你可能会说这很明显,但在许多可以传递强类型结构的情况下,我已经看到 IntPtr 被使用。不要在面向 public 的方法中使用 out 参数。在大多数情况下,这是不必要的 - 您可以 return 值。

    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. Enums. WinApi 使用大量枚举值作为参数或 return 值。作为 C 风格的枚举,它们实际上作为简单整数传递(returned)。但 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;
        }
    }
    

P.S.: 并且不要忘记正确的函数签名(位数和其他问题)、类型编组、布局属性和字符集(还有,不是忘记使用 DllImport(... SetLastError = true) is of utmost importance). http://www.pinvoke.net/ 通常会有所帮助,但它并不总是提供最好的签名。

P.S.1: 我建议您将 NativeMethods 组织成一个 class ,因为它很快就会变成一个大量难以管理的完全不同的方法,而是将它们分组为单独的 classes(我实际上使用一个 partial root class 和嵌套的 classes 用于每个功能区域 -有点乏味的打字,但更好的上下文和 Intellisense)。对于 class 名称,我只使用相同的 classification MSDN 用于对 API 函数进行分组。与 GetSystemInfo 一样,它是 "System Information Functions"


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