.NET互操作来回复制数组数据,还是固定数组?

avo*_*avo 10 .net c# c++ com com-interop

我有这个COM方法签名,在C#中声明:

void Next(ref int pcch,
          [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]
          char[] pchText);
Run Code Online (Sandbox Code Playgroud)

我称之为:

int cch = 100;
var buff = new char[cch];
com.Next(ref cch, buff);
Run Code Online (Sandbox Code Playgroud)

.NET互操作层是否首先将整个数组复制到临时的非托管内存缓冲区,然后将其复制回来?或者数组是否自动固定并通过引用传递?

为了尝试,我在COM对象(C++)中做到了这一点:

*pcch = 1;
pchText[0] = L'A';
pchText[1] = L'\x38F'; // '?'
Run Code Online (Sandbox Code Playgroud)

'?'回来时检查buff[1]C#时会回来.但我不认为这是一个强有力的证据,证明阵列固定,而不是来回复制.

Han*_*ant 9

这并不总是很容易辨别,特别是当你使用无效声明时.char []不能作为LPWStr封送,它必须是LPArray.现在CharSet属性起作用,因为你没有指定它,char []将被编组为8位char [],而不是16位wchar_t [].编组的数组元素大小不同(它不是"blittable"),因此编组器必须复制数组.

非常不受欢迎,特别是考虑到您的C++代码需要wchar_t.在这个特定情况下,一个非常简单的方法是在数组中没有得到任何回报.如果通过复制对数组进行封送处理,则必须明确告诉编组程序在调用后需要将数组复制回来.您必须[In, Out]在参数上应用该属性.你会得到中文.

判断数组是否通过复制进行编组的常用方法是使用调试器.在C#程序中启用非托管调试.在调用中设置断点以及在本机函数的第一个语句中设置断点.当第一个断点点击时,使用Debug + Windows + Memory + Memory 1.将buff放入地址框并将显示切换为"4-byte Integer".您将看到数组对象的地址,4字节类型句柄,4字节数组长度和数组内容本身.因此,您知道如果未复制数组,则传递的地址是显示的地址加上8.

按F5继续,本机功能中的断点命中.查看pchText参数,调试器会告诉您它的地址.如果匹配则编组器只是传递一个指针.如果没有,那么你得到了一个数组的副本.


nos*_*tio 6

我们来做一个小实验.首先,让我们将您的COM方法更改为这样(在C++中):

STDMETHODIMP CComObject::Next(ULONG* pcch, int* addr, OLECHAR* pbuff)
{
    pbuff[0] = L'A';
    pbuff[1] = L'\x38F';
    *addr = (int)pbuff;
    *pcch = 1;
    return S_OK;
}
Run Code Online (Sandbox Code Playgroud)

然后,更改C#方法签名:

void Next(ref uint pcch, out IntPtr addr, 
    [In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]
    char[] pbuff); 
Run Code Online (Sandbox Code Playgroud)

最后,像这样测试:

uint cch = 10;
var buff = new char[cch];
IntPtr addr1;

unsafe
{
    fixed (char* p = &buff[0])
    {
        addr1 = (IntPtr)p;
    }
}

IntPtr addr2;
com.Next(ref cch, out addr2, buff);
Console.WriteLine(addr1 == addr2);
Run Code Online (Sandbox Code Playgroud)

正如所料,addr1 == addr2true.因此,显然数组在传递给COM时会被固定而不是被复制.

也就是说,我找不到任何将此作为CLR实现的硬性要求的文档.例如,Mono可能会或可能不会这样.

  • @PauloMadeira,我的答案假设同一个COM公寓.线程间和进程间COM编组要复杂得多,但至少.NET-to-COM互操作部分不会创建中间缓冲区(在上面的代码中).然后,COM marshaller可能正在创建一个,但它也不知道它处理.NET对象. (2认同)