3

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.

Mr.C64
  • 41,637
  • 14
  • 86
  • 162
  • Why you want to us VARIANT? it is will be marshaled as Object type in C#. You should use BYTE when creating SafeArray. – Matt Nov 04 '16 at 20:47
  • @Matt: There are other clients that understand `SAFEARRAY(VARIANT)` but not `SAFEARRAY(BYTE)`. And anyway it sounds weird to me that a `CopyTo` solves that. Why do I get wrong numbers in the first place, and the correct ones only after a copy? And why the VS debugger gets the correct numbers? Is it because it copies too? – Mr.C64 Nov 04 '16 at 20:48
  • I guess but maybe it has something to do with C++ sending a pointer back (so you read pointer bytes instead of actual values it is pointing at). And the CopyTo copies the contents of the pointer thus fixing your result. – Measurity Nov 04 '16 at 21:13
  • You are fibbing in your pinvoke declaration. You *say* it returns a byte[] but it actually returns object[]. Hilarity ensues. Lying about types can be a good strategy in pinvoke declarations, it certainly is not here. – Hans Passant Nov 06 '16 at 20:34
  • @HansPassant I did that because I tried before with string[] and SAFEARRAY(VARIANT) of BSTRs and it worked fine. – Mr.C64 Nov 06 '16 at 20:46
  • Apples and oranges, a byte needs to be boxed to be stored in an object, a string doesn't. The C# compiler has no idea that it needs to unbox the byte. – Hans Passant Nov 06 '16 at 21:11
  • @HansPassant Thanks. I missed this boxing/unboxing step. If you write that as answer, I'd be happy to up vote. – Mr.C64 Nov 06 '16 at 21:56

1 Answers1

3
[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[].

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536