如何在 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# 代码调用的最令人愉快的方法。
他们是:
- 类型不那么强 - 通常充斥着
IntPtr
和 Byte[]
参数。
- 容易出错 - 很容易传递一些未正确初始化的参数,例如长度错误的缓冲区,或者某些字段未初始化为该结构大小的结构...
- 显然,如果出现问题,不要抛出异常 - 他们的消费者有责任检查 return 代码或
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
。
不要只是 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());
}
}
如果可以使用 ref/out
传递的普通结构,请不要使用 IntPtr
或 Byte[]
。你可能会说这很明显,但在许多可以传递强类型结构的情况下,我已经看到 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;
}
}
Enums. WinApi 使用大量枚举值作为参数或 return 值。作为 C 风格的枚举,它们实际上作为简单整数传递(returned)。但 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;
}
}
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.
我在 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# 代码调用的最令人愉快的方法。
他们是:
- 类型不那么强 - 通常充斥着
IntPtr
和Byte[]
参数。 - 容易出错 - 很容易传递一些未正确初始化的参数,例如长度错误的缓冲区,或者某些字段未初始化为该结构大小的结构...
- 显然,如果出现问题,不要抛出异常 - 他们的消费者有责任检查 return 代码或
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
。不要只是 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()); } }
如果可以使用
ref/out
传递的普通结构,请不要使用IntPtr
或Byte[]
。你可能会说这很明显,但在许多可以传递强类型结构的情况下,我已经看到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; } }
Enums. WinApi 使用大量枚举值作为参数或 return 值。作为 C 风格的枚举,它们实际上作为简单整数传递(returned)。但 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; } }
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.