我在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)' 由于其保护级别而无法访问
那么如何在没有错误或警告的情况下完成这项工作?
提前感谢您提供任何帮助,因为我已经圈了好几个小时了!
答案 0 :(得分:10)
您肯定会同意PInvoke方法不是从C#代码调用的最令人愉快的事情。
他们是:
IntPtr
和Byte[]
参数。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));
}
}
如果您可以使用safe handle,请不要使用IntPtr
。
不要只从包装器方法返回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());
}
}
如果您可以使用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;
}
}
枚举。 WinApi使用大量枚举值作为参数或返回值。作为C风格的枚举,它们实际上是作为简单整数传递(返回)的。但是C#枚举实际上只不过是整数,所以假设你有set proper underlying type,你将更容易使用方法。
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)
是utmost importance)。 http://www.pinvoke.net/可能经常有所帮助,但它并不总能提供最好的签名。
PS1:我建议你将NativeMethods
组织成一个班级,因为它很快就会成为一个非常难以管理的一堆完全不同的方法,而是将它们组合成一个单独的类(我实际上使用一个partial
根类和每个功能区的嵌套类 - 更多的单调乏味,但更好的上下文和智能感知。对于类名,我只使用MSDN用于分组API函数的相同分类。与GetSystemInfo类似,它是“系统信息功能”
因此,如果您应用所有这些建议,您将能够创建一个健壮,易于使用的本机包装库,隐藏所有不必要的复杂性和容易出错的构造,但对于任何知道这一点的人来说,这看起来都非常熟悉原始API。