Search code examples
c#c++bytecom-interopsafearray

Marshaling a SAFEARRAY of VARIANTs that are BYTEs to C#


I create a SAFEARRAY storing VARIANTs that are BYTEs in C++.

When this structure is marshaled to C#, a weird thing happens.

If I print the content of this structure in C# to a WinForms ListBox, e.g.:

byte data[]
TestSafeArray(out data);

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

I get some numbers that seem unrelated to the original ones. Moreover, each time I run the C# client for a new test, I get a different set of numbers.

Note that if I inspect the content of that data array with the Visual Studio debugger, I get the correct numbers, as the following screenshot shows:

VS debugger shows the correct numbers

However, if I CopyTo the marshaled data array to a new one, I get the correct numbers:

        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! **
        }

This is the C++ repro code I use to build the SAFEARRAY (this function is exported from a native 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;
}

And this is the C# P/Invoke I used:

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

Note that if in C++ I create a SAFEARRAY storing BYTEs directly (instead of a SAFEARRAY(VARIANT)), I get the correct values immediately in C#, without the intermediate CopyTo operation.


Solution

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

    You fibbed. You told the marshaller that you wanted an array of variants, indeed compatible with what the C++ compiler produced. It will dutifully produce an object[], object is the standard marshaling for a VARIANT. The elements of the array are boxed bytes.

    That did not fool the debugger, it ignored the program declaration and looked at the array type and discovered object[], readily visible in your screenshot. So it properly accessed the boxed bytes. And it did not fool Array.CopyTo(), it takes an Array argument so is forced to look at the element type. And properly converted the boxed bytes to bytes, it knows how to do that.

    But the C# compiler is fooled badly. It has no idea that it needs to emit an unbox instruction. Not actually sure what goes wrong, you are probably getting the low byte of the object address. Pretty random indeed :)

    Fibbing in pinvoke declarations can be very useful. Works just fine in this particular case if the array contains actual objects, like strings, arrays or interface pointers. The probable reason the marshaller doesn't scream bloody murder. Not here, the boxing trips it up bad. You'll have to fix the declaration, use object[].