为了方便查阅,我将之前所有的文章放入了COM的专栏中:
COM编程攻略zhuanlan.zhihu.com上一篇,我讲述了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-variantdocs.microsoft.comtypedef 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 appsdocs.microsoft.com我们可以先只留意带有[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的时候:
- 调用VariantInit初始化
- 设置vt,设置它的值
- 使用它
- 不用的时候,调用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 appsdocs.microsoft.com因此,我们可以将CComVariant看为“智能VARIANT”,我们只需要给它传入适当的值,它自己来处理VARIANT的生命周期,并且它符合我们绝大部分需求。
在OLE自动化中,VARIANT大多是由外部引擎创建,例如VB引擎创建了个字符串类型的VARIANT,其生命周期由创建者(引擎)管理。我们的C++程序拿到VARIANT后,并不会对它进行VariantClear操作。如果我们用CComVariant对其进行复制,那么也只是创建了一个副本,接口的引用计数+1,在CComVariant释放的时候引用计数-1,达到平衡。VARIANT类型,就是COM中的一种变量类型协议,只要明白了上面的规则,便可以很轻松写出跨语言的COM程序了。
下一篇:
Froser:COM编程攻略(十一 COM设施之智能指针CComPtr)zhuanlan.zhihu.com