See the question and my original answer on StackOverflow

What I would do is define a simple VARIANT structure like this:

[StructLayout(LayoutKind.Sequential)]
public struct VARIANT
{
    public ushort vt;
    public ushort r0;
    public ushort r1;
    public ushort r2;
    public IntPtr ptr0;
    public IntPtr ptr1;
}

And the interface like this;

[Guid("39c16a44-d28a-4153-a2f9-08d70daa0e22"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface MyInterface
{
    VARIANT getAttributeAsVARIANT([MarshalAs(UnmanagedType.BStr)] string strAttributeName);
}

Then, add an extension method somewhere in a static class like this, so the caller can have the same coding experience using MyInterface:

public static object getAttribute(this MyInterface o, string strAttributeName)
{
    return VariantSanitize(o.getAttributeAsVARIANT(strAttributeName));
}

private static object VariantSanitize(VARIANT variant)
{
    const int VT_PTR = 26;
    const int VT_I8 = 20;

    if (variant.vt == VT_PTR)
    {
        variant.vt = VT_I8;
    }

    var ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf<VARIANT>());
    try
    {
        Marshal.StructureToPtr(variant, ptr, false);
        return Marshal.GetObjectForNativeVariant(ptr);
    }
    finally
    {
        Marshal.FreeCoTaskMem(ptr);
    }
}

This will do nothing for normal variants, but will just patch it for VT_PTR cases.

Note this only works if the caller and the callee are in the same COM apartement.

If they are not, you will get the DISP_E_BADVARTYPE error back because marshaling must be done, and by default, it will be done by the COM universal marshaler (OLEAUT) which only support Automation compatible data types (just like .NET).

In this case, theoratically, you could replace this marshaler by another one (at COM level, not at NET level), but that would mean to add some code on C++ side and possibly in the registry (proxy/stub, IMarshal, etc.).