将 BYTE 的 VARIANT 的 SAFEARRAY 编组到 C#

Marshaling a SAFEARRAY of VARIANTs that are BYTEs to C#

我创建了一个 SAFEARRAY 存储 VARIANTs,在 C++ 中是 BYTEs。

当这个结构被编组到 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 数组的内容,我会得到 正确的 数字,如以下屏幕截图所示:

但是,如果我 CopyTo 编组 data 数组到一个新数组,我得到 正确的数字 :

        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++ 中我创建一个 SAFEARRAY 存储 BYTEs 直接 (而不是 SAFEARRAY(VARIANT)),我得到在 C# 中立即更正值,无需中间 CopyTo 操作。

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

你撒谎了。你告诉编组器你想要一个变体数组,确实与 C++ 编译器生成的兼容。它将尽职地产生一个对象[],对象是 VARIANT 的标准封送处理。数组的元素是 boxed 字节。

这并没有骗过调试器,它忽略了程序声明并查看了数组类型并发现了 object[],在您的屏幕截图中很容易看到。所以它正确地访问了装箱的字节。它并没有骗过 Array.CopyTo(),它接受一个数组参数,因此被迫查看元素类型。并正确地将装箱字节转换为字节,它知道该怎么做。

但 C# 编译器被骗得很惨。它不知道它需要发出拆箱指令。实际上不确定出了什么问题,您可能正在获取对象地址的低字节。确实很随机:)

在 pinvoke 声明中撒谎可能非常有用。如果数组包含实际对象,如字符串、数组或接口指针,则在这种特殊情况下工作得很好。编组员不尖叫血腥谋杀的可能原因。不在这里,拳击把它绊倒了。您必须修复声明,使用 object[].