c# - 如何在类库中正确调用 P/Invoke 方法?

标签 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 会给出警告:

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.



行。将可访问性更改为 internal 为:
[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# 代码中调用的最愉快的事情的说法。

他们是:

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

  • 与这些问题相比,FxCop 警告只是一个微不足道的检查器问题。

    所以,你可以做什么?处理这三个问题,FxCop 就会自行解决。

    这些是我建议你做的事情:
  • 不要直接公开任何 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));
        }
    }
    
  • 不要使用 IntPtr如果你可以使用一些 safe handle .
  • 不要只返回 Boolean(U)Int32来自包装方法 - 检查包装方法内的返回类型,并在需要时抛出异常。如果您想以无异常方式使用方法,请提供 Try -like 版本,它将清楚地表明它是一种无异常方法。
    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());
        }
    }
    
  • 不要使用 IntPtrByte[]如果您可以使用 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;
        }
    }
    
  • 枚举。 WinApi 使用大量的枚举值作为参数或返回值。作为 C 风格的枚举,它们实际上是作为简单整数传递(返回)的。但是 C# 枚举实际上也不过是整数,所以假设你有 set proper underlying type ,您将拥有更容易使用的方法。
  • 比特/字节旋转 - 每当您看到获取某些值或检查它们的正确性需要一些掩码时,您就可以确定使用自定义包装器可以更好地处理它。有时用 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;
        }
    }
    

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

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

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

    关于c# - 如何在类库中正确调用 P/Invoke 方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35818620/

    相关文章:

    android - 找不到 native 方法 org.opencv.core.mat.n_mat-Android

    c# - .NET Double.TryParse(num, format, cultureinfo, out) 错误

    .net - Powershell、PInvoke 和 GetLastError

    javascript - 有没有办法克隆 javascript 中的 native 函数,例如 window.alert 或 document.write

    c# - PInvoke - 找不到指定的模块。如何检查缺少的依赖项?

    .net - 解释一下这个功能可能会出现什么问题(如果有的话)

    c# - MarshalAs 属性案例研究

    c# -++运算符线程安全吗?

    c# - 什么是 List.Add 的渐近复杂度?

    c# - 从纹理创建法线贴图背后的逻辑是什么?