【问题标题】:How to correctly call P/Invoke methods in a class library?如何在类库中正确调用 P/Invoke 方法?
【发布时间】:2016-06-19 12:29:30
【问题描述】:

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

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

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

【问题讨论】:

  • FxCop 抱怨,因为公开原生方法被认为是不好的风格。要以这种方式重构您的程序集,并且仍然对外界隐藏 PInvokes,您必须将它们设为内部,并为您希望访问的每个其他程序集添加 InternalsVisibleTo 属性到该程序集。
  • 或者,当然,不要使用单独的程序集,只需使用带有 P/Invokes 的单个源(作为内部)并将其包含在每个需要它们的项目中(例如作为 svn 外部,或引用共享位置)。
  • @Zastai 我是这些概念的新手 :( 举个例子会很有帮助。谢谢。我特别喜欢你的第二个想法,但不知道该怎么做。

标签: c# pinvoke native-methods


【解决方案1】:

您肯定会同意 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. 如果可以使用一些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. 如果可以使用ref/out 传递的普通结构,请不要使用IntPtrByte[]。您可能会说这很明显,但在许多可以传递强类型结构的情况下,我看到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;
        }
    }
    

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

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


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

【讨论】:

  • 您在 p/invoke 调用中添加了后缀 Native,.net 会将其去掉还是您忘记在 DllImport.EntryPoint 调用中设置名称?
  • @ScottChamberlain 谢谢,忘了那个。
  • @EugenePodskal 非常简洁的注意事项!谢谢。
猜你喜欢
  • 1970-01-01
  • 2011-04-20
  • 1970-01-01
  • 2014-02-24
  • 2011-05-02
  • 1970-01-01
  • 2010-10-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多