使用 ATL/COM 将托管字节 [] 转换为非托管字节数组

Posted

技术标签:

【中文标题】使用 ATL/COM 将托管字节 [] 转换为非托管字节数组【英文标题】:Managed byte[] to unmanaged byte array using ATL/COM 【发布时间】:2012-11-08 14:46:19 【问题描述】:

我想使用 ATL/COM 将一些图像数据从 C# 代码传递到非托管 C++

从 C# 代码方面我做这样的事情:

void SendFrame([In, MarshalAs(UnmanagedType.SafeArray, SafeArraySubType=VarEnum.VT_UI1)] ref byte[] frameData);

但我不确定我应该如何在我的 C++ 代码中处理这个函数。

现在我有这样的东西:

_ATL_FUNC_INFO OnSendFrameDef =  CC_STDCALL, VT_EMPTY, 1,  VT_SAFEARRAY | VT_UI1  ;

void __stdcall OnSendFrame(SAFEARRAY* ppData)

   BYTE* pData;
   SafeArrayAccessData(ppData, (void **) &pData);

   // Doing some stuff with my pData

   SafeArrayUnaccessData(ppData);

谁能给我一些建议,让我如何让这件事发挥作用?

谢谢。

【问题讨论】:

您需要删除 ref 关键字,这样您就不会通过 SAFEARRAY**,休息应该很好。一个普通的 byte[] 和一个额外的参数来传递数组大小也可以工作并节省开销。 【参考方案1】:

由于 SAFEARRAY 已经编组到您的非托管代码,并且您正在使用 ATL,您可以使用 CComSafeArray class,因为它在使用 SAFEARRAY 时处理所有清理工作实例:

_ATL_FUNC_INFO OnSendFrameDef =  CC_STDCALL, VT_EMPTY, 1, 
     VT_SAFEARRAY | VT_UI1  ;

void __stdcall OnSendFrame(SAFEARRAY* ppData)

    // Wrap in a CComSafeArray.
    // On the stack means all calls to cleanup
    // will be cleaned up when the stack
    // is exited.
    CComSafeArray<byte> array;
    array.Attach(ppData);

    // Work with the elements, get the first one.
    byte b = array.GetAt(0);

    // And so on.  The destructor for CComSafeArray
    // will be called here and cleaned up.

【讨论】:

感谢 casperOne,但我仍然做错了,我在“atlcom.h”文件(第 4722 行)中收到带有“断言失败”的消息框。看起来像“DispCallFunc”函数返回“E_INVALIDARG”也许我的“_ATL_FUNC_INFO”是错误的?我的 .tlh 内容看起来像这样~: struct __declspec(uuid("401d2950-7c0e-3ac7-9beb-85ea798feb2d")) ITestStreamEvents : IDispatch HRESULT OnSendFrame (SAFEARRAY * frameData ); ; @Gediminas 您需要使用此信息更新主要问题; cmets 中的代码不能很好地工作。【参考方案2】:

我已经成功实现了我的目标!有兴趣的朋友:

我的事件处理程序描述符如下所示:

_ATL_FUNC_INFO Stream::OnStreamFrameCallbackDef =  CC_STDCALL, VT_EMPTY, 1,  VT_DISPATCH  ;

我的 C++ 函数:

void __stdcall Stream::OnStreamFrameCallback(IDispatch* pFrame)

   // NOTE that this "IStreamFramePtr" is COM's Ptr of my "IStreamFrame"
   MyCOM::IStreamFramePtr pStreamFrame = pFrame;

   // Thanks casperOne♦ for this:
   CComSafeArray<byte> array;
                   array.Attach(pStreamFrame->GetBuffer());

   // Now I can do stuff that I need...
   byte* pBuffer = &array.GetAt(0);

我的 .tlh 文件中的“IStreamFrame”如下所示:

struct __declspec(uuid("1f6efffc-0ac7-3221-8175-5272a09cea82"))
IStreamFrame : IDispatch

    __declspec(property(get=GetWidth))
    long Width;
    __declspec(property(get=GetHeight))
    long Height;
    __declspec(property(get=GetBuffer))
    SAFEARRAY * Buffer;

    long GetWidth ( );
    long GetHeight ( );
    SAFEARRAY * GetBuffer ( );
;

在我的 C# 代码中,我有这样的内容:

[ComVisible(true)]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)]
public interface IStreamFrame

    int     Width    [return: MarshalAs(UnmanagedType.I4)]         get; 
    int     Height   [return: MarshalAs(UnmanagedType.I4)]         get; 
    byte[]  Buffer   [return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType=VarEnum.VT_UI1)]  get; 
;


[ComVisible(false)]
public delegate void StreamFrameCallback(IStreamFrame frame);

[ComVisible(true)]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)]
public interface IMyCOMStreamEvents

    [DispId(1)]
    void OnStreamFrameCallback([In, MarshalAs(UnmanagedType.IDispatch)] IStreamFrame frame);

似乎一切正常。但如果有人有任何建议或发现我做错了什么,请告诉我。

谢谢。

【讨论】:

如果您使用 IDL 文件来描述您的接口,您会省去很多麻烦。使用 midl.exe,您将生成 C/C++ 接口、代理和存根,最后生成一个 TLB(类型库)文件,Visual C++ 的 #import 指令和 .NET 的 tlbimp.exe 都知道如何轻松处理。 TLB 文件通常也由 Microsoft 以外的许多其他开发工具处理,这将使您的最终结果更好地为将来的互操作做好准备。最后,它会为您花在学习 MIDL 上的时间带来回报。【参考方案3】:

我强烈建议您创建一个IDL file 到design your COM interfaces。

鉴于您在回答中的示例,一个相当小的 IDL 文件可能是这样的:

import "oaidl.idl"; 

[object,
 uuid(1f6efffc-0ac7-3221-8175-5272a09cea82),
 dual,
 oleautomation]
interface IStreamFrame : IDispatch 
    [propget]
    HRESULT Width([out, retval] long *pWidth);
    
    [propget]
    HRESULT Height([out, retval] long *pHeight);
    
    [propget]
    HRESULT Buffer([out, retval] SAFEARRAY(byte) *pBuffer);
;

[uuid(1f6efffc-0ac7-3221-8175-5272a09cea83)]
library ContosoStreamFrame 
    importlib("stdole32.tlb");
    
    interface IStreamFrame;
;

然后您使用midl.exe 生成带有 C/C++ 接口的 .h、用于 C/C++ 链接的 CLSID 和 IID 常量的 _i.c、用于 RPC 注册的 dlldata.c、带有代理的 _p.c和存根编组的东西和一个 .tlb ,它通常是 .idl 文件的解析表示。这在documentation 中有更好的描述。

编辑:似乎有no way to avoid the C/C++ file generation。

EDIT2:我刚刚找到了一种解决方法,使用nul 作为您不想要的输出文件。比如下面的命令只生成file.tlb

midl.exe /header nul /iid nul /proxy nul /dlldata nul file.idl

注意:如果您的IStreamFrame 接口不打算跨进程使用,请将local 属性添加到接口。

在 C/C++ 中,您可以使用生成的特定文件,或 #import TLB 文件。在 .NET 中,您可以在 TLB 文件上运行 tlbimp.exe,这会生成一个 .NET 程序集。

如果您的项目以 .NET 为中心,您也可以使用 tlbexp.exe。但是,这将要求您了解 .NET COM 注释以及它们在 IDL 方面的含义,因此我不确定以牺牲大量装饰为代价以另一种语言保存一个额外的源文件是否有任何好处接口和类定义中的噪音。如果您希望在源代码级别完全控制类和接口,并且希望在 .NET 代码上尽可能简单(阅读、优化可用性和速度),这可能是一个不错的选择。

最后,您可以通过创建项目在 Visual Studio 中自动执行所有这些操作。如果您使用 IDL 方法,请添加调用 midl.exetlbimp.exe 的自定义构建步骤,并使依赖项目依赖此项目以获得正确的构建顺序。如果您使用 .NET 方法,请添加一个调用 tlbexp.exe 的自定义构建步骤,并使依赖的 C/C++ 项目依赖于该项目。

编辑:如果您不需要从 midl.exe 生成的 C/C++ 文件,您可以将 del 命令添加到特定输出文件的自定义构建步骤中。

EDIT2:或使用上述nul 解决方法。

当类型库已经存在时,一种常用的方法是使用 Visual Studio 将其导入 .NET。但这样一来,如果您更新 IDL 文件,您必须记住重新生成 TLB 并再次导入它。

【讨论】:

【参考方案4】:

您是否考虑过 C++/CLI?在您的 C++/CLI ref class 中,您将编写一个成员函数:

void SendFrame(cli::array<System::Byte> frameData)

    pin_ptr<System::Byte> p1 = &frameData[0];
    unsigned char *p2 = (unsigned char *)p1;

    // So now p2 is a raw pointer to the pinned array contents

【讨论】:

没有迹象表明可以从 COM 服务器更改代码,它可能必须保持 COM 服务器。 谢谢丹尼尔,但我不能使用 CLI

以上是关于使用 ATL/COM 将托管字节 [] 转换为非托管字节数组的主要内容,如果未能解决你的问题,请参考以下文章

将带有单位的数字转换为非人类可读格式

ATL/COM : 在 IDL 文件中我们可以创建一个可以用作返回类型的类

在 ATL COM dll 中调用了错误的方法

如何在 C# 中将固定字节/字符 [100] 转换为托管字符 []?

使用 P/Invoke 时为非托管代码存储数据

使用存储过程将文件 URL 转换为文件字节?