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

10

我有一个在C#中声明的COM方法签名:

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

我这样称呼它:

int cch = 100;
var buff = new char[cch];
com.Next(ref cch, buff);

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

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

*pcch = 1;
pchText[0] = L'A';
pchText[1] = L'\x38F'; // 'Ώ'

当我检查 C# 中的 buff[1] 时,我确实会得到 'Ώ' 返回值。但我不认为这是数组被固定而不是来回复制的有力证据。


3
这并没有回答如何精确实现所有内容的问题,但在这种情况下,pchText参数是一个隐式输入参数。之所以它能够工作,仅仅因为你在进程内(而且它可能只是作为指针实现的)。如果使用一个进程外的COM服务器,它就不能工作了(你的C++代码只会操纵该进程中的存根内存)。如果它真的是一个输出或引用参数,你需要将其添加到ref或out中。 - Simon Mourier
@SimonMourier,抱歉我没有澄清,这只是一个进程内的DLL COM对象,没有代理/存根。 - avo
每个COM接口都应该能够在进程内或进程外工作。 - Simon Mourier
@SimonMourier,我同意,我的意思是在我们的项目中它只在STA线程上进程内使用。不过,我遵循了你的建议并添加了[In, Out]:http://stackoverflow.com/q/25039290/2674222。 - avo
2个回答

9
有时候很难分辨,特别是当你使用无效声明的时候。char[]不能作为LPWStr进行编组,而必须是LPArray。现在CharSet属性发挥了作用,因为你没有指定它,所以char[]将被编组为8位char[],而不是16位wchar_t[]。编组后的数组元素大小不同(它不是“可平铺的”),所以编组器必须复制数组。
这非常不理想,特别是你的C++代码需要wchar_t。在这种情况下,非常容易判断是否有任何东西返回到数组中。如果通过复制来编组数组,则必须显式地告诉编组器调用后需要将数组复制回来。您需要在参数上应用[In, Out]属性。这样就可以得到中文。
通常判断数组是否通过复制进行编组的方法是使用调试器。在C#程序中启用非托管调试。在调用和本地函数的第一条语句上设置断点。当第一个断点命中时,使用Debug + Windows + Memory + Memory 1。将buff放入地址框中,并将显示切换为“4字节整数”。您将看到数组对象的地址、4字节类型句柄、4字节数组长度和数组内容本身。因此,如果未复制数组,则传递的地址是显示的地址加上8。
按F5继续,本地函数中的断点被命中。查看pchText参数,调试器会告诉您它的地址。如果匹配,则编组器只是传递了一个指针。如果不匹配,则你得到了数组的副本。

1
嗯,CharSet 的问题在于它适用于整个函数,而不是单个参数。而且它也不会影响 char[],只会影响指定为 LPTStr 的字符串参数。实现字符串缓冲区的正确方法是使用 StringBuilder……您可以预先分配所需大小,并指定 LPStrLPWStr - Ben Voigt
Hans,再次感谢,它实际上是一个“LPArray”,我只是盲目地从我们之前遇到的答案中复制了旧代码。 - avo
实际上,char 很好用。我认为 CharSet 对于 p/invoke 很重要,但对于 COM 不重要(正如我们之前讨论的那样)。例如,我尝试了 Ώ (pchText[0] = L'\x38F'),它成功地回传到了 C#。 - avo

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;
}

接下来,更改C#方法签名:

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

最后,可以像这样进行测试:
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);

正如预期的那样,addr1 == addr2true。因此,显然当数组传递给 COM 时,它确实被固定而不是复制。

话虽如此,我找不到任何文档将其作为 CLR 实现的硬性要求。例如,这对于 Mono 可能是真实的,也可能不是。


1
这里有一些文档:http://msdn.microsoft.com/zh-cn/library/23acw07k(v=vs.110).aspx - Simon Mourier
我无法复制这个问题,地址不同。编辑:我可以在同一公寓中复制。但是,无论是否在同一公寓中,数据似乎都没有被写入到“char []”中。 - acelent
我发现原因了,我正在使用(在IDL术语中)[out] LPWSTR *pszData,对其进行了CoTaskMemAlloc。那个内存地址被写入,而不是实际的字符。 - acelent
2
@PauloMadeira,我的回答假设使用相同的COM公寓。线程间和进程间的COM编组更加复杂,但至少.NET到COM互操作部分不会创建中间缓冲区(在上面的代码中)。然后,COM编组器可能正在创建一个,但它不知道它正在处理.NET对象。 - noseratio - open to work

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接