将一个由BYTE组成的VARIANT类型的SAFEARRAY转换为C#

3
我创建了一个在C++中存储字节型VARIANT的SAFEARRAY。
当这个结构体被传递到C#时,出现了奇怪的问题。
如果我在C#中将该结构体的内容打印到WinForms的ListBox中,例如:
byte data[]
TestSafeArray(out data);

lstOutput.Items.Clear();    
foreach (byte x in data)
{
    lstOutput.Items.Add(x); // Strange numbers
}

我得到了一些与原始数字似乎无关的数字。此外,每次我为新测试运行C#客户端时,我都会得到不同的数字集合。
请注意,如果我使用Visual Studio调试器检查该data数组的内容,我将获得正确的数字,如下面的截图所示:

VS debugger shows the correct numbers

然而,如果我将编组的数据数组CopyTo到一个新数组中,我会得到正确的数字:
        byte[] data;
        TestSafeArray(out data);

        // Copy to a new byte array
        byte[] byteData = new byte[data.Length];
        data.CopyTo(byteData, 0);

        lstOutput.Items.Clear();
        foreach (byte x in byteData)
        {               
            lstOutput.Items.Add(x); // ** WORKS! **
        }

这是我用来构建 SAFEARRAY 的 C++ 代码(此函数是从本地 DLL 导出的):

extern "C" HRESULT __stdcall TestSafeArray(/* [out] */ SAFEARRAY** ppsa)
{
    HRESULT hr = S_OK;
    try 
    {
        const std::vector<BYTE> v{ 11, 22, 33, 44 };

        const int count = static_cast<int>(v.size());
        CComSafeArray<VARIANT> sa(count);

        for (int i = 0; i < count; i++)
        {
            CComVariant var(v[i]);

            hr = sa.SetAt(i, var);
            if (FAILED(hr))
            {
                return hr;
            }
        }

        *ppsa = sa.Detach();
    } 
    catch (const CAtlException& e)
    {
        hr = e;
    }

    return hr;
}

这是我使用的C# P/Invoke:

[DllImport("NativeDll.dll", PreserveSig = false)]
private static extern void TestSafeArray(
    [Out, MarshalAs(UnmanagedType.SafeArray, 
                    SafeArraySubType = VarEnum.VT_VARIANT)]
    out byte[] result);

请注意,如果我在 C++ 中创建一个直接存储 BYTE 的 SAFEARRAY(而不是 SAFEARRAY(VARIANT)),则可以立即在 C# 中获得正确的值,无需中间的 CopyTo 操作。

为什么要使用 VARIANT?因为在 C# 中,它将被编组为 Object 类型。创建 SafeArray 时应使用 BYTE。 - Matt
你在PInvoke声明中撒谎了。你“说”它返回一个byte[],但实际上它返回的是object[]。这会导致一些滑稽的情况发生。在PInvoke声明中撒谎关于类型可能是一个好策略,但在这里显然不是。 - Hans Passant
@HansPassant 我这样做是因为之前我尝试使用string[]和SAFEARRAY(VARIANT)的BSTR,而且它也能正常工作。 - Mr.C64
苹果和橙子,一个字节需要打包才能存储在对象中,而字符串则不需要。C#编译器不知道它需要取消打包字节。 - Hans Passant
@HansPassant 谢谢。我错过了这个装箱/拆箱步骤。如果您将其写为答案,我很乐意点赞。 - Mr.C64
显示剩余2条评论
1个回答

3
[Out, MarshalAs(UnmanagedType.SafeArray, 
                SafeArraySubType = VarEnum.VT_VARIANT)]
out byte[] result);

你欺骗了marshaller,告诉它你需要一个与C++编译器生成的数组兼容的变体数组。它将会忠实地产生一个object[],object是VARIANT的标准封送处理方式。数组的元素是封装好的字节。
但这并没有愚弄调试器,它忽略了程序声明并查看了数组类型,在你的截图中很容易看到它是object[]。所以它正确地访问了封装好的字节。而且它也没有愚弄Array.CopyTo(),它需要一个Array参数,因此被迫查看元素类型,并正确地将封装好的字节转换为字节,它知道如何做到这一点。
但是C#编译器被彻底愚弄了。它不知道它需要发出一个unbox指令。我不确定会发生什么错误,你可能得到对象地址的低字节。非常随机 :)
在pinvoke声明中欺骗可以非常有用。如果数组包含实际对象(如字符串、数组或接口指针),则在这种特定情况下可以正常工作。这可能是marshaller没有报错的原因。但在这里,封箱会使它失灵。你必须修复声明,使用object[]

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