variant每個成員必賦值_COM程式設計攻略(十 COM設施之VARIANT)
技術標籤:variant每個成員必賦值
為了方便查閱,我將之前所有的文章放入了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