C# 程序无法从 C++ COM 程序中获取字节数组

Posted

技术标签:

【中文标题】C# 程序无法从 C++ COM 程序中获取字节数组【英文标题】:C# Program Can't Get Byte Array Back From A C++ COM Program 【发布时间】:2021-12-08 13:01:45 【问题描述】:

我似乎无法在从 COM C++ 程序填充的 C# 程序中获取字节数组。 C# 程序包含对 C++ DLL 的引用,并通过以下方式实例化:

_wiCore = new WebInspectorCoreLib.WICore();

实际调用

uint imageSize = *image byte count*;  // set to size of image being retrieved
var arr = new byte[imageSize];
_wiCore.GetFlawImage(1, 0, ref imageSize, out arr);

C++ IDL:

[id(5)] HRESULT GetFlawImage([in] ULONG flawID, [in] USHORT station, [in, out] ULONG *pImageSize, [out] BYTE *pImageBytes);

这会正确返回图像大小,但数组中没有任何内容。我还尝试了签名(pImageBytes 的额外间接级别):

[id(5)] HRESULT GetFlawImage([in] ULONG flawID, [in] USHORT station, [in, out] ULONG *pImageSize, [out] BYTE **pImageBytes);

在 C# 中传入一个 IntPtr,但这会返回包含图像字节地址的内存地址,而不是图像字节。

对我做错了什么有什么想法吗?

【问题讨论】:

也许您需要将 C# 内存提供给 C++ 来填充? 当 COM 调用需要返回动态分配的内存时,您不能使用 c++ 堆,因此不要使用 new 来分配图像内存,请使用 ::CoTaskMemAlloc docs.microsoft.com/en-us/windows/win32/api/combaseapi/…。另一种选择是返回一个安全的字节数组:docs.microsoft.com/en-us/archive/msdn-magazine/2017/march/… 你为什么不使用SAFEARRAY?然后将其声明为retval 而不是out 我同意@Charlieface 的观点——SAFEARRAY唯一在 COM 中表示数组的正确方法。您最终可能想要使用 DCOM,然后尝试将指针传递给多个数据,这将严重破坏,而 SAFEARRAY 会正常工作。 @BenVoigt - 这不是真的。您基本上可以在 COM 中使用任何类型(如果使用是进程外的,您必须注意输入/输出/等注释和/或提供代理存根)。事实是,高级语言(阅读:不同于 C/C++)通常更容易使用自动化类型(VARIANT 类型等)。 【参考方案1】:

有多种方法可以从 C++ 传回数组。

例如,您可以像尝试那样使用原始字节数组。它可以工作,但在 .NET 中不是很实用,因为它不是 .NET 喜欢的 COM automation type。

所以,假设我们有这个 .idl:

interface IBlah : IUnknown

    HRESULT GetBytes([out] int *count, [out] unsigned char **bytes);

这是一个示例原生实现:

STDMETHODIMP CBlah::GetBytes(int* count, unsigned char** bytes)

    if (!count || !bytes)
        return E_INVALIDARG;

    *count = numBytes;
    *bytes = (unsigned char*)CoTaskMemAlloc(*count);
    if (!*bytes)
        return E_OUTOFMEMORY;

    for (unsigned char i = 0; i < *count; i++)
    
        (*bytes)[i] = i;
    
    return S_OK;

还有一个示例 C# 调用代码(请注意,当它不是 COM 自动化类型时,.NET 类型库导入器除了指针之外什么都不知道,所以它只是盲目地将参数定义为 IntPtr):

var obj = (IBlah)Activator.CreateInstance(myType);

// we must allocate a pointer (to a byte array pointer)
var p = Marshal.AllocCoTaskMem(IntPtr.Size);
try

    obj.GetBytes(out var count, p);

    var bytesPtr = Marshal.ReadIntPtr(p);
    try
    
        var bytes = new byte[count];
        Marshal.Copy(bytesPtr, bytes, 0, bytes.Length);
        // here bytes is filled
    
    finally
    
        // here, we *must* use the same allocator than used in native code
        Marshal.FreeCoTaskMem(bytesPtr);
    

finally

    Marshal.FreeCoTaskMem(p);

注意:这在进程外场景中不起作用,因为 .idl 不完整以支持此等。

或者您可以使用 COM 自动化类型,例如 SAFEARRAY(或包装 VARIANT)。这也将允许您将它与其他语言一起使用(例如 VB/VBA、脚本引擎等)

所以,我们可以有这个 .idl:

HRESULT GetBytesAsArray([out] SAFEARRAY(BYTE)* array);

这个示例本机实现(稍微复杂一点,因为 COM 自动化并不适用于 C/C++,而是适用于 VB/VBA/Scripting 对象...):

STDMETHODIMP CBlah::GetBytesAsArray(SAFEARRAY** array)

    if (!array)
        return E_INVALIDARG;

    // create a 1-dim array of UI1 (byte)
    *array = SafeArrayCreateVector(VT_UI1, 0, numBytes);
    if (!*array)
        return E_OUTOFMEMORY;

    unsigned char* bytes;
    HRESULT hr = SafeArrayAccessData(*array, (void**)&bytes); // check errors
    if (FAILED(hr))
    
        SafeArrayDestroy(*array);
        return hr;
    

    for (unsigned char i = 0; i < numBytes; i++)
    
        bytes[i] = i;
    

    SafeArrayUnaccessData(*array);
    return S_OK;

正如预期的那样,示例 C# 代码要简单得多:

var obj = (IBlah)Activator.CreateInstance(myType);
obj.GetBytesAsArray(out var bytesArray);

【讨论】:

这是我需要的。谢谢你。 (虽然我不确定你是否想调用 SafeArrayDestroy(*array);。似乎分配它的代码应该释放它)。 @user1524258 - 发生错误时必须调用 SafeArrayDestroy,是的 [out, retval]SAFEARRAY 更有意义 @Charlieface - 我认为这不会“更有意义”。这只是我没有做出的更高级别的语义选择(仅对理解它的语言有用),因为这不是这里的主题。

以上是关于C# 程序无法从 C++ COM 程序中获取字节数组的主要内容,如果未能解决你的问题,请参考以下文章

我们如何在 C# 中从 C++ DLL 中获取所有方法?

C#、C++、WinAPI - 从另一个进程获取窗口数

将 C# 3D 数组作为 1D 字节数组传递给 C++ 库

如何使用 C# 从 C++ 应用程序获取调用堆栈?

从 C++ 库方法返回的“char*”获取 C# 字符串?

在 C++ 中通过 'recv' 和 'MSG_PEEK' 获取套接字中可用的字节数