variant每个成员必赋值_COM编程攻略(十 COM设施之VARIANT)

为了方便查阅,我将之前所有的文章放入了COM的专栏中:

COM编程攻略​zhuanlan.zhihu.com
c5f38eb042a0cd9e7ed1731d293d8b98.png

上一篇,我讲述了COM中的标准字符串BSTR

Froser:COM编程攻略(九 COM设施之BSTR)​zhuanlan.zhihu.com

这一篇,我来谈一谈COM中的变量通行证:VARIANT类型。

一、什么是VARIANT类型

VARIANT类型,可以跨语言(VB, C#, Java, C++)来表示任意一种类型。和BSTR一样,它也是微软约定好的一种协议。遵循这种协议,我们的变量可以正确地被支持COM的语言所认识。

VARIANT类型的定义可以参照MSDN:

https://docs.microsoft.com/zh-cn/windows/win32/api/oaidl/ns-oaidl-variant​docs.microsoft.com
typedef struct tagVARIANT {
  union {
    struct {
      VARTYPE vt;
      WORD    wReserved1;
      WORD    wReserved2;
      WORD    wReserved3;
      union {
        LONGLONG     llVal;
        LONG         lVal;
        BYTE         bVal;
        SHORT        iVal;
        FLOAT        fltVal;
        DOUBLE       dblVal;
        VARIANT_BOOL boolVal;
        VARIANT_BOOL __OBSOLETE__VARIANT_BOOL;
        SCODE        scode;
        CY           cyVal;
        DATE         date;
        BSTR         bstrVal;
        IUnknown     *punkVal;
        IDispatch    *pdispVal;
        SAFEARRAY    *parray;
        BYTE         *pbVal;
        SHORT        *piVal;
        LONG         *plVal;
        LONGLONG     *pllVal;
        FLOAT        *pfltVal;
        DOUBLE       *pdblVal;
        VARIANT_BOOL *pboolVal;
        VARIANT_BOOL *__OBSOLETE__VARIANT_PBOOL;
        SCODE        *pscode;
        CY           *pcyVal;
        DATE         *pdate;
        BSTR         *pbstrVal;
        IUnknown     **ppunkVal;
        IDispatch    **ppdispVal;
        SAFEARRAY    **pparray;
        VARIANT      *pvarVal;
        PVOID        byref;
        CHAR         cVal;
        USHORT       uiVal;
        ULONG        ulVal;
        ULONGLONG    ullVal;
        INT          intVal;
        UINT         uintVal;
        DECIMAL      *pdecVal;
        CHAR         *pcVal;
        USHORT       *puiVal;
        ULONG        *pulVal;
        ULONGLONG    *pullVal;
        INT          *pintVal;
        UINT         *puintVal;
        struct {
          PVOID       pvRecord;
          IRecordInfo *pRecInfo;
        } __VARIANT_NAME_4;
      } __VARIANT_NAME_3;
    } __VARIANT_NAME_2;
    DECIMAL decVal;
  } __VARIANT_NAME_1;
} VARIANT;

它主要由一个变量类型(枚举值)和一个联合体(变量内容)组成。

变量类型指明了这个VARIANT是表示数字、整形还是其它类型。变量内容就是表示它实际的值。我们在使用一个VARIANT时,先要拿出它的vt成员,看看它是什么类型,然后再从union中拿对应的成员。

VARTYPE的定义如下所示:

enum VARENUM
    {
        VT_EMPTY	= 0,
        VT_NULL	= 1,
        VT_I2	= 2,
        VT_I4	= 3,
        VT_R4	= 4,
        VT_R8	= 5,
        VT_CY	= 6,
        VT_DATE	= 7,
        VT_BSTR	= 8,
        VT_DISPATCH	= 9,
        VT_ERROR	= 10,
        VT_BOOL	= 11,
        VT_VARIANT	= 12,
        VT_UNKNOWN	= 13,
        VT_DECIMAL	= 14,
        VT_I1	= 16,
        VT_UI1	= 17,
        VT_UI2	= 18,
        VT_UI4	= 19,
        VT_I8	= 20,
        VT_UI8	= 21,
        VT_INT	= 22,
        VT_UINT	= 23,
        VT_VOID	= 24,
        VT_HRESULT	= 25,
        VT_PTR	= 26,
        VT_SAFEARRAY	= 27,
        VT_CARRAY	= 28,
        VT_USERDEFINED	= 29,
        VT_LPSTR	= 30,
        VT_LPWSTR	= 31,
        VT_RECORD	= 36,
        VT_INT_PTR	= 37,
        VT_UINT_PTR	= 38,
        VT_FILETIME	= 64,
        VT_BLOB	= 65,
        VT_STREAM	= 66,
        VT_STORAGE	= 67,
        VT_STREAMED_OBJECT	= 68,
        VT_STORED_OBJECT	= 69,
        VT_BLOB_OBJECT	= 70,
        VT_CF	= 71,
        VT_CLSID	= 72,
        VT_VERSIONED_STREAM	= 73,
        VT_BSTR_BLOB	= 0xfff,
        VT_VECTOR	= 0x1000,
        VT_ARRAY	= 0x2000,
        VT_BYREF	= 0x4000,
        VT_RESERVED	= 0x8000,
        VT_ILLEGAL	= 0xffff,
        VT_ILLEGALMASKED	= 0xfff,
        VT_TYPEMASK	= 0xfff
    } ;

我们可以在头文件中,清晰的看到每个类型的含义:

/*
 * VARENUM usage key,
 *
 * * [V] - may appear in a VARIANT
 * * [T] - may appear in a TYPEDESC
 * * [P] - may appear in an OLE property set
 * * [S] - may appear in a Safe Array
 *
 *
 *  VT_EMPTY            [V]   [P]     nothing
 *  VT_NULL             [V]   [P]     SQL style Null
 *  VT_I2               [V][T][P][S]  2 byte signed int
 *  VT_I4               [V][T][P][S]  4 byte signed int
 *  VT_R4               [V][T][P][S]  4 byte real
 *  VT_R8               [V][T][P][S]  8 byte real
 *  VT_CY               [V][T][P][S]  currency
 *  VT_DATE             [V][T][P][S]  date
 *  VT_BSTR             [V][T][P][S]  OLE Automation string
 *  VT_DISPATCH         [V][T]   [S]  IDispatch *
 *  VT_ERROR            [V][T][P][S]  SCODE
 *  VT_BOOL             [V][T][P][S]  True=-1, False=0
 *  VT_VARIANT          [V][T][P][S]  VARIANT *
 *  VT_UNKNOWN          [V][T]   [S]  IUnknown *
 *  VT_DECIMAL          [V][T]   [S]  16 byte fixed point
 *  VT_RECORD           [V]   [P][S]  user defined type
 *  VT_I1               [V][T][P][s]  signed char
 *  VT_UI1              [V][T][P][S]  unsigned char
 *  VT_UI2              [V][T][P][S]  unsigned short
 *  VT_UI4              [V][T][P][S]  ULONG
 *  VT_I8                  [T][P]     signed 64-bit int
 *  VT_UI8                 [T][P]     unsigned 64-bit int
 *  VT_INT              [V][T][P][S]  signed machine int
 *  VT_UINT             [V][T]   [S]  unsigned machine int
 *  VT_INT_PTR             [T]        signed machine register size width
 *  VT_UINT_PTR            [T]        unsigned machine register size width
 *  VT_VOID                [T]        C style void
 *  VT_HRESULT             [T]        Standard return type
 *  VT_PTR                 [T]        pointer type
 *  VT_SAFEARRAY           [T]        (use VT_ARRAY in VARIANT)
 *  VT_CARRAY              [T]        C style array
 *  VT_USERDEFINED         [T]        user defined type
 *  VT_LPSTR               [T][P]     null terminated string
 *  VT_LPWSTR              [T][P]     wide null terminated string
 *  VT_FILETIME               [P]     FILETIME
 *  VT_BLOB                   [P]     Length prefixed bytes
 *  VT_STREAM                 [P]     Name of the stream follows
 *  VT_STORAGE                [P]     Name of the storage follows
 *  VT_STREAMED_OBJECT        [P]     Stream contains an object
 *  VT_STORED_OBJECT          [P]     Storage contains an object
 *  VT_VERSIONED_STREAM       [P]     Stream with a GUID version
 *  VT_BLOB_OBJECT            [P]     Blob contains an object 
 *  VT_CF                     [P]     Clipboard format
 *  VT_CLSID                  [P]     A Class ID
 *  VT_VECTOR                 [P]     simple counted array
 *  VT_ARRAY            [V]           SAFEARRAY*
 *  VT_BYREF            [V]           void* for local use
 *  VT_BSTR_BLOB                      Reserved for system use
 */

例如,VT_EMPTY表示一个空的VARIANT,VT_NULL表示一个数据库的NULL,VT_I2表示一个2个字节的整型,对应着VARIANT里面的iVal。

微软也准备了专门的VARTYPE文档:

VARENUM (wtypes.h) - Win32 apps​docs.microsoft.com
efbc71a8696cd24a16217a3c47d392a0.png

我们可以先只留意带有[V]的类型,它表示我们所使用的VARIANT类型可能会用到。VARIANT的主要使用场景是在IDispatch中。如我们之前的文章中说到,用户可以通过VBA脚本来操作我们的COM服务程序,那么VB和C++中间的参数的传输,也就是通过VARIANT类型来。例如,VB中传入了一个字符串类型,那么C++中接受到的就是一个类型为VT_BSTR的VARIANT。

二、使用VARIANT类型

COM中提供了一堆API来操作(创建、拷贝、销毁等)VARIANT类型。

我们可以通过VariantInit(VARIANTARG* pvarg)来初始化一个VARIANT(VARIANTARG是VARIANT别名)变量

它将变量初始化为VT_EMPTY。它用于初始化一个新的变量,而不是清除一个现成的VARIANT变量。

通过反汇编,我们看到VariantInit执行了下面的操作:

778B4170  mov         edi,edi  
778B4172  push        ebp  
778B4173  mov         ebp,esp  
778B4175  mov         eax,dword ptr [ebp+8]  
778B4178  xor         ecx,ecx  
778B417A  mov         dword ptr [eax],ecx  
778B417C  mov         dword ptr [eax+4],ecx  
778B417F  mov         dword ptr [eax+8],ecx  
778B4182  mov         dword ptr [eax+0Ch],ecx  
778B4185  pop         ebp  
778B4186  ret         4  

可以看出,它只是简单地把VARIANT中的vt、wReserved1、wReserved2、wReserved3重置为0。

我们通过VariantClear(VARIANTARG * pvarg)来清理一个VARIANT对象。

清理工作,将会根据vt拿出对应的成员进行操作。例如,假如它是VT_BSTR,那么就会调用SysFreeString。你可以自己判断是否有必要调用VariantClear,例如,假如你想将VT_I4的类型改为其它类型,那么你不需要调用VariantClear,因为它不涉及到释放堆上面的内容。当调用完VariantClear后,变量的vt将会被设置为VT_EMPTY。

需要注意的是,有一个特殊的类型VT_BYREF,可以和指针类的VARTYPE结合,例如VT_DISPATCH|VT_BYREF,VT_UNKNOWN|VT_BYREF。它表示这个VARIANT是引用传递,其内部实现为包含了另外一个IDispatch对象或者IUnknown对象的void*指针。它并没有为所持有的对象增加计数,所以,当有VT_BYREF时,VariantClear并不会导致这些被持有的对象减计数。

我们通过VariantCopy(VARIANTARG *pvargDest, const VARIANTARG *pvargSrc)来将pvargSrc中的内容拷贝到pvargDest中。如果pvargDest事先有内容,那么则使用VariantClear来释放它。

VARIANT a, b;
VariantInit(&a);
VariantInit(&b);
...
VariantCopy(&a, &b); // 释放a中的内容,然后a<-b

拷贝过程严格遵循着对象类型的拷贝语义,例如,如果拷贝的是一个IUnknown接口,那么拷贝后将会调用IUnknown::AddRef增加引用。有一个例外是,如果它的指针类型组合了VT_BYREF,如同上面说的那样,我们只不过是拥有了一个IUnknown的void*指针,VARIANT内部并不会将它当成IUnknown来增加引用,所以在拷贝的时候就不会调用AddRef了。这个和我们在写C++时传递shared_ptr时,传值和传引用的行为一致。值拷贝会使得引用计数+1,而传引用并不会。

由此我们可以大概知道,VT_BYREF把VARIANT标记为仅仅是一个指针地址,它在拷贝和释放的时候,不会对对象类型语义进行分析,而是当成一个值那样,什么都不做地释放。

我们可以得出一下结论,当需要使用一个VARIANT的时候:

  1. 调用VariantInit初始化
  2. 设置vt,设置它的值
  3. 使用它
  4. 不用的时候,调用VariantClear来进行释放

和BSTR一样,这意味着我们需要管理一个VARIANT的生命周期。对于一些分配在堆上的VARIANT,如VT_BSTR或者VT_DISPATCH等,忘记调用了VariantClear,将会造成内存泄露。任何时候,如果我们不使用某一个VARIANT了,都有必要调用VariantClear释放它可能分配在堆上的资源。

如同CComBSTR一样,ATL也提供了一个包装类来管理VARIANT,它就是CComVariant。

三、ATL的CComVariant的实现

CComVariant重载了非常多的版本的构造函数,通过传入不同变量的类型,来决定自己的vt。

class CComVariant :
    public tagVARIANT
{
// Constructors
public:
    CComVariant() throw()
    {
        // Make sure that variant data are initialized to 0
        memset(this, 0, sizeof(tagVARIANT));
        ::VariantInit(this);
    }
    ~CComVariant() throw()
    {
        HRESULT hr = Clear();
        ATLASSERT(SUCCEEDED(hr));
        (hr);
    }
    CComVariant(_In_ const VARIANT& varSrc) ATLVARIANT_THROW()
    {
        vt = VT_EMPTY;
        InternalCopy(&varSrc);
    }
    CComVariant(_In_z_ LPCOLESTR lpszSrc) ATLVARIANT_THROW()
    {
        vt = VT_EMPTY;
        *this = lpszSrc;
    }
    CComVariant(_In_z_ LPCSTR lpszSrc) ATLVARIANT_THROW()
    {
        vt = VT_EMPTY;
        *this = lpszSrc;
    }
    CComVariant(_In_ bool bSrc) throw()
    {
        vt = VT_BOOL;
        boolVal = bSrc ? ATL_VARIANT_TRUE : ATL_VARIANT_FALSE;
    }

    CComVariant(_In_ BYTE nSrc) throw()
    {
        vt = VT_UI1;
        bVal = nSrc;
    }
    CComVariant(_In_ short nSrc) throw()
    {
        vt = VT_I2;
        iVal = nSrc;
    }
...
    HRESULT Clear()
    {
        return ::VariantClear(this);
    }
};

CComVariant是直接继承于VARIANT类的,所以它可以直接使用VARIANT成员。

它的析构函数非常简单,调用Clear()方法,也就是调用VariantClear()。

它的构造函数大概可以分为几类。

第一类是没有参数的构造函数,简单地调用了VariantInit。

第二类是传入值变量的构造函数,例如传入BYTE, short等,vt将分别设置为VT_UI1, VT_I2,且设置到对应的成员中。

第三类转调了自身的operator=,例如当接收一个LPCOLESTR (BSTR)时,它转调到了下面的方法:

    CComVariant& operator=(_In_z_ LPCOLESTR lpszSrc) ATLVARIANT_THROW()
    {
        if (vt != VT_BSTR || bstrVal != lpszSrc)
        {
            ClearThrow();

            vt = VT_BSTR;
            bstrVal = ::SysAllocString(lpszSrc);

            if (bstrVal == NULL && lpszSrc != NULL)
            {
                vt = VT_ERROR;
                scode = E_OUTOFMEMORY;
#ifndef _ATL_NO_VARIANT_THROW
                AtlThrow(E_OUTOFMEMORY);
#endif
            }
        }
        return *this;
    }

它会在堆上面构造一个BSTR,并且维护它的生命周期。当然,在赋值之前,它会先清理掉自己。

如果是指针类的赋值,它会在类型里面加上VT_BYREF,例如:

    CComVariant& operator=(_In_ BYTE* pbSrc) ATLVARIANT_THROW()
    {
        if (vt != (VT_UI1|VT_BYREF))
        {
            ClearThrow();
            vt = VT_UI1|VT_BYREF;
        }
        pbVal = pbSrc;
        return *this;
    }

但是如果赋值的是一个IUnknown*或者IDispatch*,那么它不会带上VT_BYREF,而是会选择进行生命周期托管,调用其AddRef:

    CComVariant& operator=(_Inout_opt_ IDispatch* pSrc) ATLVARIANT_THROW()
    {
        if (vt != VT_DISPATCH || pSrc != pdispVal)
        {
            ClearThrow();

            vt = VT_DISPATCH;
            pdispVal = pSrc;
            // Need to AddRef as VariantClear will Release
            if (pdispVal != NULL)
                pdispVal->AddRef();
        }
        return *this;
    }

    CComVariant& operator=(_Inout_opt_ IUnknown* pSrc) ATLVARIANT_THROW()
    {
        if (vt != VT_UNKNOWN || pSrc != punkVal)
        {
            ClearThrow();

            vt = VT_UNKNOWN;
            punkVal = pSrc;

            // Need to AddRef as VariantClear will Release
            if (punkVal != NULL)
                punkVal->AddRef();
        }
        return *this;
    }

这样,在清理的时候,对象的Release()将会被调用。

无论如何,赋一个新值时,它一定会清理旧的值。

它还封装了另外一些有用的方法,例如拷贝(VariantCopy,在赋值另一个VARIANT的时候会被调用),更改变量类型(VariantChangeType)等:

VariantChangeType function (oleauto.h) - Win32 apps​docs.microsoft.com
efbc71a8696cd24a16217a3c47d392a0.png

因此,我们可以将CComVariant看为“智能VARIANT”,我们只需要给它传入适当的值,它自己来处理VARIANT的生命周期,并且它符合我们绝大部分需求。

在OLE自动化中,VARIANT大多是由外部引擎创建,例如VB引擎创建了个字符串类型的VARIANT,其生命周期由创建者(引擎)管理。我们的C++程序拿到VARIANT后,并不会对它进行VariantClear操作。如果我们用CComVariant对其进行复制,那么也只是创建了一个副本,接口的引用计数+1,在CComVariant释放的时候引用计数-1,达到平衡。VARIANT类型,就是COM中的一种变量类型协议,只要明白了上面的规则,便可以很轻松写出跨语言的COM程序了。

下一篇:

Froser:COM编程攻略(十一 COM设施之智能指针CComPtr)​zhuanlan.zhihu.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值