在 C# 中使用 P/Invoke 调用 Win API,当 _Out_ 参数可以为 NULL 或非 NULL 时

本文关键字:NULL 参数 Out 或非 API Win 调用 Invoke | 更新日期: 2023-09-27 18:32:22

我是C#的新手,我正在通过编写一些小工具来学习C#。

有许多 Windows API 的指针参数可以是 NULL 或非 NULL,具体取决于不同的用例。我的问题是,如何在 DllImport 中声明这些参数?

例如:

LONG QueryDisplayConfig(
  _In_       UINT32 Flags,
  _Inout_    UINT32 *pNumPathArrayElements,
  _Out_      DISPLAYCONFIG_PATH_INFO *pPathInfoArray,
  _Inout_    UINT32 *pNumModeInfoArrayElements,
  _Out_      DISPLAYCONFIG_MODE_INFO *pModeInfoArray,
  _Out_opt_  DISPLAYCONFIG_TOPOLOGY_ID *pCurrentTopologyId
);

Flags QDC_DATABASE_CURRENT时,最后一个参数pCurrentTopologyId不能为空。当Flags是其他值时,pCurrentTopologyId必须为 null。

如果参数声明为"out IntPtr"或"ref IntPtr",以便 API 可以更改引用的内存。但是,如果按照 API 的要求将其传递 IntPtr.Zero,则 API 调用将返回ERROR_NOACCESS。

[DllImport(user32_FileName, SetLastError=true)]
internal static extern int QueryDisplayConfig(
    [In] QDC_FLAGS Flags,
    [In, Out] ref UInt32 pNumPathArrayElements, 
    [Out] DISPLAYCONFIG_PATH_INFO[] pPathInfoArray,
    [In, Out] ref UInt32 pNumModeInfoArrayElements, 
    [Out] DISPLAYCONFIG_MODE_INFO[] pModeInfoArray,
    out IntPtr pCurrentTopologyId
);

如果参数声明为"IntPtr",则可以将 IntPtr.Zero 作为 NULL 指针传递。但是,如果传递 IntPtr,API 调用也将返回ERROR_NOACCESS。

[DllImport(user32_FileName, SetLastError=true)]
internal static extern int QueryDisplayConfig(
    [In] QDC_FLAGS Flags,
    [In, Out] ref UInt32 pNumPathArrayElements, 
    [Out] DISPLAYCONFIG_PATH_INFO[] pPathInfoArray,
    [In, Out] ref UInt32 pNumModeInfoArrayElements, 
    [Out] DISPLAYCONFIG_MODE_INFO[] pModeInfoArray,
    IntPtr pCurrentTopologyId
);

我不希望声明不同版本的 extern 函数,尤其是当不同参数组合的数量可能很多时。

有什么建议吗?

在 C# 中使用 P/Invoke 调用 Win API,当 _Out_ 参数可以为 NULL 或非 NULL 时

当你有一个作为枚举的可选参数时,就像你在这里一样,那么我认为除了声明两个单独的重载之外别无选择。用于传递 null 的重载声明参数,如下所示:

IntPtr pCurrentTopologyId

调用此重载时,始终传递IntPtr.Zero

另一个重载,即

在传递非 null 值时调用的重载,如下所示声明参数:

out DISPLAYCONFIG_TOPOLOGY_ID pCurrentTopologyId

其中DISPLAYCONFIG_TOPOLOGY_ID是基类型为 int 的 C# enum

您的代码弄错了第二个变体,因为您将其声明为 out IntPtr pCurrentTopologyId 。好吧IntPtr 64 位(8 字节而不是 4 字节)的大小是错误的。

如果你只想声明一个p/invoke而不使用重载,那么你只需将工作转移到其他地方。为此,您必须选择第一个选项:

IntPtr pCurrentTopologyId

当你想通过IntPtr.Zero时很好。但是当你需要传递枚举变量的地址时,你需要使用 GCHandleAddrOfPinnedObject 来固定你的变量。所有这些都完全可能,但更多的样板。所以,任你挑选!