1. 程式人生 > 其它 >variant每個成員必賦值_COM程式設計攻略(十 COM設施之VARIANT)

variant每個成員必賦值_COM程式設計攻略(十 COM設施之VARIANT)

技術標籤: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