Search code examples
c++comvariantbstr

Can I call VariantChangeType on an input BSTR VARIANT?


I have a COM object written in C++ with a method using the following signature. Assume that the variant contains a BSTR (just VT_BSTR, not VT_BYREF | VT_BSTR).

HRESULT myfunc(/*[in]*/ VARIANT param)

I want to change the type to something else. If the first parameter of VariantChangeType is the same as the second, the "variant will be converted in place."

So, can I convert in place?

HRESULT myfunc(/*[in]*/ VARIANT param)
{
    VariantChangeType(&param, param, 0, VT_I4);
}

Or should I copy to a second variant?

HRESULT myfunc(/*[in]*/ VARIANT param)
{
    VARIANT temp;
    VariantInit(&temp);
    VariantChangeType(&temp, param, 0, VT_I4);
}

My understanding is that the latter is required, since the former would free the BSTR, which is owned by the client and should be freed by the client.


Solution

  • Using VariantChangeType with a second variant is required, although it may not be obvious.

    Even though the variant is passed by value, any pointers contained in the variant point to the same memory addresses. Since a BSTR is a pointer, that means the original address of the BSTR is passed into the function, just as if the parameter were BSTR instead of VARIANT.

    Using VariantChangeType (in-place) or VariantClear will trigger SysFreeString, meaning that the original variant (owned by the caller) still contains the address of the BSTR but that address no longer holds the BSTR.

    From the "Variant Manipulation Functions" documentation...

    When releasing or changing the type of a variant with the VT_BSTR type, SysFreeString is called on the contained string.

    The reason this isn't obvious is that this code seems to work, even though everything I described above says it shouldn't.

    #ifndef WIN32_LEAN_AND_MEAN
    #define WIN32_LEAN_AND_MEAN
    #endif // !WIN32_LEAN_AND_MEAN
    #include <Windows.h>
    #include <OleAuto.h>
    
    HRESULT myfuncbad(/*[in]*/ VARIANT param)
    {
        // In-place conversion
        VariantChangeType(&param, &param, 0, VT_I4);
        return S_OK;
    }
    
    HRESULT myfuncgood(/*[in]*/ VARIANT param)
    {
        VARIANT temp;
        VariantInit(&temp);
        // Copy and convert into a new VARIANT
        VariantChangeType(&temp, &param, 0, VT_I4);
        VariantClear(&temp);
        return S_OK;
    }
    
    int main()
    {
        VARIANT input;
        VariantInit(&input);
        V_BSTR(&input) = SysAllocString(L"1");
        V_VT(&input) = VT_BSTR;
    
        myfuncgood(input);
        wprintf(L"Memory location of BSTR = 0x%x\n", (unsigned)(V_BSTR(&input)));
        wprintf(L"Contents of BSTR = %s\n", V_BSTR(&input));
    
        myfuncbad(input);
        wprintf(L"Memory location of BSTR = 0x%x\n", (unsigned)(V_BSTR(&input)));
        wprintf(L"Contents of BSTR = %s\n", V_BSTR(&input));
    }
    

    This code runs and outputs something like the following

    Memory location of BSTR = 0x2d1af0c
    Contents of BSTR = 1
    Memory location of BSTR = 0x2d1af0c
    Contents of BSTR = 1
    

    But why? Turns out that BSTR allocations are cached. So, even when VariantChangeType (in-place) or VariantClear is called, the BSTR allocation may stick around for a while. It will be immediately obvious on the variant passed into those functions, but any "by-value" copy of the variant may still see the BSTR for some time.

    Regardless, the BSTR has, technically, been freed by myfuncbad and should no longer be referenced by the caller. Also, calling VariantClear on the original variant may cause errors.

    Additional reading