關於com自定義參數的傳遞

 

COM深入理解(下)

——方法參數類型爲CRuntimeClass*void*

    本文上篇已經說明了類對象實際是一個結構實例,並且爲了實現進程間傳遞類對象指針以達到引用的目的,需要爲此類專門編寫一個代理類,並在傳遞時例示(即實例化)其一個對象以實現代理對象。而此代理類必定分成兩部分,即一部分的成員函數專門在客戶進程被調用,另一部分專門在組件進程被調用以實現進程間的數據傳遞進而將客戶的調用命令原封不動地傳遞給組件對象以實現客戶操作組件對象。
   
上面的做法實際就是編寫自定義彙集操作時應該乾的事,只不過還需照着COM的幾個附加規定來做,如必須實現IMarshal接口等。本文說明如何爲這樣的類型傳遞編寫標準的代理/佔位組件以跨進程傳遞類對象的指針(使用MIDL來完成)。

    爲了在客戶端生成一個代理對象,必須將某些信息傳遞過去,然後在客戶端根據傳遞的信息構建一個代理對象。在IDL語言的類型定義中,沒有類這種類型,因此是不可能讓接口方法的參數類型爲某個自定義類的指針。但是的確有此需要,則只能將類對象指針轉成某種IDL中識別的類型,最好的候選人就是void*,然後藉助MIDL生成的代碼將構建代理對象的信息傳遞過去。
    void*
不帶有任何語義,其僅表示一個地址,因此在IDL中傳遞void*是錯誤的,因爲MIDL無法根據void*所帶的語義確定應該如何彙集其指向內存中的內容。但是MIDL還是提供了多種途徑來解決這個問題的,下面僅說明其中兩個用得最多的方法:[call_as()]屬性和[wire_marshal()]屬性。


[local][call_as()]

    [local]  接口或接口方法都可以加上[local]屬性以表示此方法或此接口中的方法不需要生成彙集代碼,進而就避免了上面由於void*不帶有任何語義而不能彙集其指向內容這個問題,因爲不需要生成彙集代碼,進而其所修飾的方法的參數可以爲void*。此屬性所修飾的方法或接口被稱爲本地方法或本地接口,因爲這些方法沒有彙集代碼,不能進行遠程調用。這在COM的標準接口中應用十分廣泛。如查看IUnknownIDL代碼,其就是一個本地接口。再如查看IClassFactory接口的IDL定義,如下:
[
    object,
    uuid(00000001-0000-0000-C000-000000000046),
    pointer_default(unique)
]
interface IClassFactory : IUnknown
{
    typedef [unique] IClassFactory * LPCLASSFACTORY;
    [local]
    HRESULT CreateInstance(
        [in, unique] IUnknown * pUnkOuter,
        [in] REFIID riid,
        [out, iid_is(riid)] void **ppvObject);
    [call_as(CreateInstance)]
    HRESULT RemoteCreateInstance(
        [in] REFIID riid,
        [out, iid_is(riid)] IUnknown ** ppvObject);
    [local]
    HRESULT LockServer(
        [in] BOOL fLock);
    [call_as(LockServer)]
    HRESULT __stdcall RemoteLockServer(
        [in] BOOL fLock);
}
   
其中的CreateInstanceLockServer就是本地函數,MIDL將不會爲這兩個函數生成彙集代碼,也就是代理/佔位代碼,其表現就是類似下面的兩個函數原型的代碼:
HRESULT STDMETHODCALLTYPE IClassFactory_LockServer_Proxy( IClassFactory * This,
                                                          BOOL fLock );
HRESULT STDMETHODCALLTYPE IClassFactory_LockServer_Stub( IClassFactory * This,
                                                         BOOL fLock );
   
也就是說,當在.idl文件中檢測到一個接口方法的定義時,MIDL都會爲這個方法生成兩個附加的函數,名字分別爲<InterfaceName>_<MethodName>_Proxy<InterfaceName>_<MethodName>_Stub,以分別作爲代理和佔位的代碼。如上面的RemoteCreateInstance,將生成IClassFactory_RemoteCreateInstance_ProxyIClassFactory_RemoteCreateInstance_Stub這麼兩個函數的聲明和定義。
   
但是當方法被[local]屬性修飾時,則不會生成上面的兩個函數的聲明和定義,因爲它們被假定一定用於直接調用,不會有彙集的需要,因此沒有彙集代碼,並被稱爲本地方法。但它們還是會被加入接口這個函數指針數組的行列,即生成的接口頭文件中依舊可以看見這類方法的聲明(但是在類型庫中卻沒有,這可以認爲是MIDL的一個BUG,不過是可以繞過的)。
    [call_as()] 
接口方法可以被加上[call_as()]屬性進行修飾,以指定此方法將被作爲括號中指定的本地方法調用的替代品,即被作爲什麼調用。它不像[local]屬性修飾的方法,其依舊會生成彙集代碼,但卻不會出現在接口中,即生成的頭文件中,看不見這類方法的聲明(但是在類型庫中卻看得見,這是一個BUG,可以通過預定義宏繞過)。此被稱爲方法別名,因爲其將兩個方法關聯了起來,其中一個([local]修飾的)是另一個([call_as]修飾的)的別名,被實際使用。
   
如前面的RemoteLockServer就帶有屬性[call_as(LockServer)]以表示此函數是當客戶調用LockServer時,並且需要進行彙集操作時調用的。將[local]修飾的方法稱爲本地版,[call_as()]修飾稱爲遠程版,則可以認爲遠程版函數解決了本地版函數沒有生成彙集代碼的問題,因爲本地版函數可能有某些特殊要求(如參數類型爲void*)而不能生成彙集代碼。
   
既然[call_as()]產生了一個函數別名,對兩個函數進行了關聯,因此必須有一種機制實現這種關聯。MIDL就是通過要求開發人員自己編寫本地版方法的彙集代碼來實現這個關聯關係。對於上面的LockServerMIDL將會爲其生成兩個函數原型,如下:
HRESULT STDMETHODCALLTYPE IClassFactory_LockServer_Proxy( IClassFactory * This,
                                                          BOOL fLock );
HRESULT __stdcall IClassFactory_LockServer_Stub( IClassFactory * This,
                                                 BOOL fLock );
   
但僅僅是原型,即聲明,沒有定義。因此開發人員需自己編寫上面兩個函數的定義。注意:雖然名字是IClassFactory_LockServer_Stub,但它的原型正好和RemoteLockServer對調,以實現將遠程版函數傳遞過來的參數再轉成本地版的參數。
   
因此關聯的過程就是:客戶調用IClassFactory_LockServer_Proxy,然後開發人員編寫此函數,並在其中將傳進來的MIDL不能或不希望被處理的參數類型轉成IClassFactory_RemoteLockServer_Proxy的參數形式,並調用之以傳遞參數。在組件端,COM運行時期庫調用開發人員編寫的IClassFactory_LockServer_Stub(注意:此函數的原型不是LockServer,而是RemoteLockServer)以將通過網絡傳過來的參數換成原始的MIDL不能或不希望被處理的參數形式,並調用傳進來的IClassFactory*參數的LockServer方法以實現調用了組件對象的方法,然後返回。下面舉個簡例:
   
有個自定義類CA,如下:
class CA
{
    long m_a, m_b;
public:
    long GetA();
    void SetA( long a );
};
   
欲在下面的接口中傳遞其對象指針:
///////////////////////abc.idl/////////////////////////
import "oaidl.idl";
import "ocidl.idl";

[
    object,
    uuid(1A201ABC-A669-4ac7-9E02-2DA772E927FC),
    pointer_default(unique)
]
interface IAbc : IUnknown
{
    [local] HRESULT GetA( [out] void* pA );
    [call_as( GetA )] HRESULT RemoteGetA( [out] long *pA, [out] long *pB );
};
   
新建一DLL工程,關掉“預編譯頭文件”編譯開關,將生成的abc_i.cabc_p.cdlldata.cabc.h加到工程中,並建立一個abc.def文件加入到工程中以導出幾個必要的用於註冊的函數,如下:
;;;;;;;;;;;;;;;;;;;;;;;;abc.def;;;;;;;;;;;;;;;;;;;;;;;;;
LIBRARY "abc"
EXPORTS
    DllCanUnloadNow     PRIVATE
    DllGetClassObject   PRIVATE
    DllRegisterServer   PRIVATE
    DllUnregisterServer PRIVATE
   
並新添加一個abc.cpp文件,如下:
///////////////////////abc.cpp/////////////////////////
#include "abc.h"
#include <new>

class CA
{
public:
    long m_a, m_b;
    long GetA();
    void SetA( long a );
};
HRESULT STDMETHODCALLTYPE IAbc_GetA_Proxy( IAbc *This, void *pA )
{
    if( !pA )
        return E_INVALIDARG;
    CA *pAA = reinterpret_cast< CA* >( pA );

//
調用遠程版的代理函數以傳遞參數,由MIDL生成
    return IAbc_RemoteGetA_Proxy( This, &pAA->m_a, &pAA->m_b );
}
HRESULT STDMETHODCALLTYPE IAbc_GetA_Stub( IAbc *This, long *pA, long *pB )
{
    void *p = CoTaskMemAlloc( sizeof( CA ) );
    if( !p )
        return E_FAIL;
    CA *pAA = new( p ) CA;  //
生成一個類對象

//
調用對象的本地方法
    HRESULT hr = This->GetA( pAA );
    if( SUCCEEDED( hr ) )
    {
        *pA = pAA->m_a;
        *pB = pAA->m_b;
    }

//
釋放資源
    pAA->~CA();
    CoTaskMemFree( p );
    return hr;
}
   
最後添加預定義宏REGISTER_PROXY_DLL_WIN32_WINNT=0x500,並連接rpcrt4.lib庫文件,確保沒有打開/TC/TP編譯開關以保證對上面的abc.cpp進行C++編譯,而對MIDL生成的.c的源文件進行C編譯。
   
使用時如下:
IAbc *pA;  //
假設已初始化
CA    a;
pA->GetA( reinterpret_cast< void* >( &a ) );
   
而組件實現的代碼如下:
STDMETHODIMP CAbc::GetA( void *pA )
{
    if( !pA )
        return E_INVALIDARG;

    *reinterpret_cast< CA* >( pA ) = m_A;
    return S_OK;
}
   
如上就實現了將類CA的對象進行傳值操作,但不是傳址操作。前面已說明,欲進行後者,必須編寫相應的代理類。先使用上面的方法將必要的信息傳遞後,再根據傳遞的信息初始化類CA的代理對象以建立連接。一般如非得已最好不要編寫代理對象,而通過將類轉成接口形式,由MIDL輔助生成代理/佔位組件以變相實現。
   
下面介紹使用[wire_marshal()]屬性進行傳值操作。


[wire_marshal()]

    前面使用方法別名機制實現了傳遞自定義數據類型,但是其是以方法爲單位進行處理的,當要多次使用某一個數據類型時,如前面的CA*,如果對每個使用到CA*的方法都進行上面的操作,很明顯地效率低下,爲此MIDL提供了[wire_marshal()]屬性(當然不止這麼一個屬性)。
    [wire_marshal()]
屬性只能用於類型定義,即typedef中,使用語法如下:
typedef [wire_marshal(wire_type)] type-specifier userm-type;
   
其將一個線類型(wire-type,即MIDL可以直接處理的類型)和一個描述類型(type-specifier,即不能或不打算被MIDL處理的特殊數據類型)相關聯,並用一個可識別名字(userm-type)標識。其和[transmit_as()]屬性類似,都是將兩個類型進行關聯,就如前面的[local][call_as()]將兩個方法進行關聯一樣,只不過[wire_marshal()]是直接將描述類型按IDL的列集格式(網絡數據描述NDR——Network Data Representation)列集到指定的緩衝區中,而[transmit_as()]還需彙集代碼在中間再轉換一次,因此[wire_marshal()]的效率要更高,只不過由於需要編寫列集代碼,因此需要了解NDR格式,處理數據對齊等問題,所以顯得麻煩和複雜。最常見的應用就是句柄的定義,如下:
typedef union _RemotableHandle switch( long fContext ) u
{
    case WDT_INPROC_CALL:   long   hInproc;
    case WDT_REMOTE_CALL:   long   hRemote;
} RemotableHandle;
typedef [unique] RemotableHandle * wireHWND;
#define DECLARE_WIREM_HANDLE(name)  /
                             typedef [wire_marshal(wire ## name)] void * name
DECLARE_WIREM_HANDLE( HWND );
   
也就是說我們常用的HWND類型是:
typedef [wire_marshal( wireHWND )] void* HWND;
   
即其在應用程序中(即客戶或組件,即代理/佔位的使用者)是void*類型,當需要傳輸時,實際是傳輸結構RemotableHandle的一個實例,而此結構是一個以fContext爲標識的聯合,實際爲8字節長。
   
爲了實現上面提到的void*RemotableHandle*的關聯,開發人員必須提供下面四個函數的定義:
unsigned long __RPC_USER < userm-type >_UserSize(  //
返回欲請求的緩衝區大小
    unsigned long __RPC_FAR *pFlags,  //
一個標誌參數,後敘
    //
給出當前已經請求的緩衝區大小,返回的大小應該以此作爲起點
    unsigned long StartingSize,
    < userm-type > __RPC_FAR * pUser_typeObject );  //
欲傳遞的描述類型的實例
unsigned char __RPC_FAR * __RPC_USER < userm-type >_UserMarshal(  //
列集
    unsigned long __RPC_FAR * pFlags,   //
標誌參數
    unsigned char __RPC_FAR * Buffer,   //
已分配的緩衝器有效指針
    < userm-type > __RPC_FAR * pUser_typeObject );  //
欲列集的描述類型的實例
unsigned char __RPC_FAR * __RPC_USER < userm-type >_UserUnmarshal(  //
散集
    unsigned long __RPC_FAR *  pFlags,   //
標誌參數
    unsigned char __RPC_FAR *  Buffer,   //
列集數據的緩衝器指針
    //
描述類型的實例指針,從列集數據中散集出描述類型後,放在此指針所指內存之中
    < userm-type > __RPC_FAR * pUser_typeObject );
void __RPC_USER < userm-type >_UserFree(  //
釋放UserUnmarshal中分配的內存
    unsigned long __RPC_FAR * pFlags,     //
標誌參數
    // UserUnmarshal
中的pUser_typeObject參數,一個描述類型的實例的指針
    < userm-type > __RPC_FAR * pUser_typeObject );
   
對於前面的HWND,開發人員就必須提供如下四個函數的定義(當然Microsoft是已經提供了的):
unsigned long __RPC_USER
    HWND_UserSize( unsigned long*, unsigned long, HWND* );
unsigned char* __RPC_USER
    HWND_UserMarshal( unsigned long*, unsigned char*, HWND* );
unsigned char* __RPC_USER
    HWND_UserUnmarshal( unsigned long*, unsigned char*, HWND* );
void __RPC_USER
    HWND_UserFree( unsigned long*, HWND* );
   
MIDL生成的彙集代碼中,遇到方法參數類型爲HWND時,發生如下事情:
    1.
調用HWND_UserSize並傳遞應用程序(客戶或組件,視HWNDin參數還是out參數)傳進來的HWND的實例以得到欲傳遞此實例需要的緩衝區大小
    2.
RPC通道上分配相應的內存塊
    3.
調用HWND_UserMarshal,依舊傳遞前面的HWND實例以及分配到的緩衝區的指針以將此HWND實例列集到緩衝區中
    4.
通過RPC通道將緩衝區內容傳遞到對方進程空間中
    5.
調用HWND_UserUnmarshal,並傳遞通過RPC通道得到的列集數據緩衝區的指針和生成的一臨時HWND實例的指針以記錄散集出來的HWND實例
    6.
以返回的HWND實例爲參數調用應用程序的方法
    7.
調用HWND_UserFree,傳遞前面因調用HWND_UserUnmarshal而生成的臨時記錄散集出的HWND實例的指針以釋放因此分配的內存
   
以上,就是[wire_marshal()]屬性對線類型和描述類型的綁定的實現。但其中漏了一點,就是標誌參數pFlags的使用。此標誌參數是一個4字節數字,其高16位是一些關於NDR格式的編碼規則,以使得NDR引擎(將填寫好的緩衝區內容按NDR格式串的規則進行排列以在網上傳輸的程序)能做出正確的數據轉換。其低16位是一個MSHCTX枚舉值,指明調用環境,是進程內還是跨進程、是遠程還是本地(具體信息還請查閱MSDN),因而可以在上面的四個函數中根據此值作出相應的優化。
   
下面爲上面的CA*實現[wire_marshal()]屬性。
   
前面已經瞭解到,CA*由於在IDL中沒有對應的類型,應該使用void*來進行傳遞,在abc.idl中增加如下代碼:
typedef struct _SA
{
    long a, b;
} *PSA;
typedef [wire_marshal( PSA )] void* PA;
   
併爲接口IAbc增加一個方法:
HRESULT SetA( [in] PA a );
   
接着在abc.cpp中增加如下代碼:
unsigned long __RPC_USER PA_UserSize( unsigned long* /* pFlags */,
                                      unsigned long  StartingSize,
                                      PA* /* ppA */ )
{
//
之所以有StartingSize,因爲此參數可能並不是第一個被列集的參數,
//
如:HRESULT SetA( [in] long tem1, [in] char tem2, [in] PA a );
//
此時的StartingSize就爲sizeof( long ) + sizeof( char )
//
而之所以還要再將其傳進來是爲了對齊需要

//
此處沒有進行對齊處理,因爲結構_SA是隻有兩個unsigned long的簡單
//
結構,無須再刻意對齊。
    return StartingSize + sizeof( _SA );
}
unsigned char* __RPC_USER PA_UserMarshal( unsigned long *pFlags,
                                          unsigned char *Buffer,
                                          PA *ppA )
{
//
按線種類(即結構_SA)的定義填衝緩衝區,注意必須按照NDR傳輸格式
//
進行填充,這裏由於_SA簡單,所以只是簡單地複製,沒有什麼對齊及一
//
致性數據的問題。關於NDR傳輸格式的詳細內容,請參考
// http://www.opengroup.org/onlinepubs/9629399/chap14.htm
    if( *pFlags & MSHCTX_INPROC )
    {
    //
是進程內調用,直接將CA*進行傳遞,而不進行拷貝
        *reinterpret_cast< void** >( Buffer ) = *ppA;
    }
    else
    {
        CA *pA = reinterpret_cast< CA* >( *ppA );
        PSA pSA = reinterpret_cast< PSA >( Buffer );
        pSA->a = pA->m_a;
        pSA->b = pA->m_b;
    }

//
返回緩衝區的有效位置,當前位置後的sizeof( _SA )個字節
    return Buffer + sizeof( _SA );
}
unsigned char* __RPC_USER PA_UserUnmarshal( unsigned long *pFlags,
                                            unsigned char *Buffer,
                                            PA *ppA )
{
    if( *pFlags & MSHCTX_INPROC )
    {
    //
是進程內調用,直接將CA*進行傳遞,而不進行拷貝
        *ppA = *reinterpret_cast< void** >( Buffer );
    }
    else
    {
        void *p = CoTaskMemAlloc( sizeof( CA ) );
        if( !p )
            return Buffer + sizeof( _SA );
        CA *pAA = new( p ) CA;  //
生成一個類對象

        PSA pSA = reinterpret_cast< PSA >( Buffer );
        pAA->m_a = pSA->a;
        pAA->m_b = pSA->b;

        *ppA = p;
    }

//
返回緩衝區的有效位置,當前位置後的sizeof( _SA )個字節
    return Buffer + sizeof( _SA );
}
void __RPC_USER PA_UserFree( unsigned long *pFlags,
                             PA *ppA )
{
    if( !( *pFlags & MSHCTX_INPROC  ) )
    {
    //
不是進程內彙集,分配了內存,釋放資源
        CA *pAA = reinterpret_cast< CA* >( *ppA );
        pAA->~CA();
        CoTaskMemFree( pAA );
    }
}
   
使用中,則:
IAbc *pA;  //
假設已初始化
CA a;
a.SetA( 654 );
PA pAA = &a;
pA->SetA( pAA );  //
或者直接pA->SetA( &a );
pA->GetA( &a );

    非常明顯,MIDL提供的可用於自定義類型傳遞的屬性很正常地不止上面幾個,如:[transmit_as()][handle]等,在此僅起拋磚引玉的作用,關於MIDL提供的其他屬性,還請參考MSDN。上面的實現方法中,都不僅僅提供了彙集自定義數據類型的渠道,還提供了優化的途徑(如上面的pFlags標誌參數)。因此在編寫代理/佔位組件時,應考慮在關鍵地方應用類似的屬性進行生成代碼的優化。

Using user defined type in COM&ATL

·  Preface

The reason I got into this is that I've rarely used any help from newsgroups or similar communities. On the other hand since I've used code provided by other developers/programmers on CodeProject and CodeGuru it seemed reasonable to join a couple of them and just have a look.

Early in May 2000 I noticed several posts about UDTs and their interaction with VB and ATL. At this point I may say I had not any real experience on the subject. As a matter of fact I've never developed professionally in COM with C++ or ATL. In addition I've learned the hard way that one cannot apply the same coding techniques one uses with C or C++ to VB. Still I consider myself novice in the COM environment.

It is true that there is very little help in implementing UDTs in COM and even less in implementing arrays of UDTs. In the past it was not even thinkable to use UDTs in COM. Nowadays there is support for UDTs in COM but there are no real example projects on how to use this feature. So a personal mail by a fellow developer inspired me to go onto this.

I am going to present a step by step approach on creating an ATL project which using UDTs to communicate with a VB Client. Using it with a C++ Client will be easy as well.

This document will proceed along with the project. I assume you are familiar with ATL, COM and VB. On the way I may present practices I use myself, which may be irrelevant to the cause of this example, but on the other hand you may have also used these practices as well or beginners may benefit from these.

Create the ATL project.

As a starting point create an ATL DLL project using the wizard. Set the name of the project to UDTDemo and accept the defaults. Now let's have a look at the generated "IDL" file.

//UDTDemo.IDL

 

import "oaidl.idl";

import "ocidl.idl";

 

[

    uuid(C21871A1-33EB-11D4-A13A-BE2573A1120F),

    version(1.0),

    helpstring("UDTDemo 1.0 Type Library")

]

library UDTDEMOLib

{

    importlib("stdole32.tlb");

    importlib("stdole2.tlb");

 

};

Modify the type library name

As you expected this, there is nothing unknown in this file so far. Well, the fact is that I do not really like the "Lib" portion added to the name of the projects I create, and I always change it before any object is being inserted into the project. This is very easy.

As a first step edit the "IDL" file and set the library name to what you like. You have only to remember that this is case sensitive when the MIDL generated code is used. The modified file is shown bellow.

//UDTDemo.IDL

 

import "oaidl.idl";

import "ocidl.idl";

 

[

    uuid(C21871A1-33EB-11D4-A13A-BE2573A1120F),

    version(1.0),

    helpstring("UDTDemo 1.0 Type Library")

]

library UDTDemo   //UDTDEMOLib

{

    importlib("stdole32.tlb");

    importlib("stdole2.tlb");

 

};

The second step is to replace any occurrence of the previous library name with the new one. The only file apart the "IDL" one, where the library name is found is the main project implementation file, "UDTDemo.cpp", where DllMain is called and the _module is initialized. You may also use the "Find in Files" command from the toolbar and search for the "UDTDEMOLib" string.

What ever way we use we have to replace the "LIBID_UDTDEMOLib" string with the "LIBID_UDTDemo" one. Mind the case of the strings. It is case sensitive.

Now you are ready to change the name of your type library to what you really like. Again keep in mind that this is not going to be trivial unless it is done before any object is added into the project, or before any early compilation of the project.

Bellow is the modified DllMain function of our project.

//UDTDemo.cpp

 

extern "C"

BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)

{

    if (dwReason == DLL_PROCESS_ATTACH)

    {

        //_Module.Init(ObjectMap, hInstance, &LIBID_UDTDEMOLib);

        _Module.Init(ObjectMap, hInstance, &LIBID_UDTDemo);

        DisableThreadLibraryCalls(hInstance);

    }

    else if (dwReason == DLL_PROCESS_DETACH)

        _Module.Term();

    return TRUE;    // ok

}

You may Compile the project now. Be sure everything is done right. In case something goes wrong you should make sure all occurrences of "UDTDEMOLib" are replaced with "UDTDemo".

Defining the structure.

An empty project is not of any use. Our purpose is to define a UDT, or struct respectively, and this is what I am going to do right now.

The demo project will handle named variables. This means we need a structure for holding the Name, and the Value of a variable. Although I haven't tested it yet, we may add a VARIANT to hold some other Special data.

The above types where chosen so as you may see the whole story, and not take any homework at all. :)

So open the UDTDemo.idl file and add these lines before the library block.

//UDTDemo.idl

 

    typedef

    [

        uuid(C21871A0-33EB-11D4-A13A-BE2573A1120F),

        version(1.0),

        helpstring("A Demo UDT variable for VB projects")

    ]

    struct UDTVariable {

        [helpstring("Special case variant")]    VARIANT  Special;

        [helpstring("Name of the variable")]    BSTR     Name;

        [helpstring("Value of the variable")]   long     Value;

    } UDTVariable;

Save and build again. Everything should compile without any problems. Well you have to follow this pace in this demo project. :)

User Defined data Types. The Theory.

Whenever a structure is created in IDL we need to specify a UUID for it so as the type library manager interfaces can get information about the UDT and access it. (I also realized why on this project :) ).

UUIDs

How is the UUID for the structure acquired? No, we do not execute the guidgen utility. My next free hack is this. It may not be approved, but it works. Go to the library section, copy the UUID of the library, and paste it after the typedef keyword of the structure inside the angle brackets. Then go to the 8th digit and subtract (one) 1.

The library     uuid(C21871A1-33EB-11D4-A13A-BE2573A1120F)

                            |

                           /./

The UDTVariable uuid(C21871A0-33EB-11D4-A13A-BE2573A1120F)

As it is documented the UUIDs are created using the current date, the current time and the unique number of the network card of the computer. Well, the time and date part resides in the first eight-digit part of the UUID. The next four-digit part is not known to me so far. The rest of it is unique and, we may say, identifies your PC. So, subtracting one (1) from the time part takes us to the past. Finally this UUID is still unique!!!

As a rule of thumb, after the library UUID is created, I add one (1) to the time part of the UUID for every interface and coclass I insert into the project. Subtract one (1) for the structures or enumerations I use. Basically the interface UUIDs are replaced and will be demonstrated later.

The only reason I get into this kind of trouble is because it is said that Windows handle consequent UUIDs in the registry faster!

More type attributes.

After the definition of the UUID for our structure we define its version number. This is a hack discovered after checking the type library of a VB created object. VB adds a version number to everything it adds to a type library. This will never be used in this project but why not use it?

Then add a help string. This brief description is very useful to everyone using our library. I recommend using it all the time.

We could also add the public keyword to make the structure visible from the library clients. This is not necessary as it will finally be implicitly visible to the clients. Clients should not be able to create any structure which might not be used in the interfaces exposed by our object.

The UDT Data members.

Let's proceed to the data members now. First every data member of our UDT must be an automation compatible type. In the simpler form, as I've conclude, in a UDT we are allowed to use only the types defined in the VARIANT union, if you have checked the code, or whatever VB allows us to use inside a variant type.

This is only for our sake to avoid creating marshaling code for our structure. Otherwise you are free to pass even a bit array with a structure :).

The data types of our UDT members were chosen so as we can expect some difficulty and make the demonstration as complete as possible.

  • long Value : a member of type long was chosen because it behaves like any other built in type. There are no extra considerations to be taken for built in types. (long, byte, double, float, DATE, VT_BOOL).

  • BSTR Name : Although strings are easily handled in VB, here we have some considerations to take into account. Creation, Initialization and Destruction of the string are good reasons to take care of and use a string in the demo.

  • VARIANT Special : This came up just now. Since we are going to do it, variants are more difficult to use than BSTR's, Not only in terms of initialization and termination, but also in checking what is the real type of the actual variants. This is not so bad!

Arrays as structure members.

At this point you should know how to declare a structure of simple types in IDL. Finally, now that you know how to declare a UDT structure to be used in VB we have to take the exercise 1 and create a UDT which holds an array of UDTs. The reason is, that arrays are also special cases, and since we haven't put an array in our structure in the first place, lets make a case with an array. Using an array of longs or what ever other type would be the same at this point of the demonstration.

//UDTDemo.idl

 

    typedef

    [

        uuid(C218719F-33EB-11D4-A13A-BE2573A1120F),

        version(1.0),

        helpstring("A Demo UDT Holding an Array of Named Variables")

    ]

    struct UDTArray {

 

        [helpstring("array of named variables")]   

                   SAFEARRAY(UDTVariable) NamedVars;

 

    } UDTArray;

As you have noticed, the only difference is that we used the SAFEARRAY declaration in the first place, but we also included the name of our newly declared UDT. This is the right way to give the COM mechanism the knowledge of what the array will be holding. At this point we have declared a UDT holding a typed array.

Declaring an array of longs it would be as simple, as declaring the following.

SAFEARRAY(long)

Performing a test.

We may compile our project once more. At this point it would be nice to create a test VB project , and add our library into this client project through the references in the project menu. Now press F2 to check what VB may see in our library. Well, nothing but the globals appears in the window.

This is due to the fact that we have declared our UDT's outside the library block in the IDL file. Well, if any declared item, (enum, UDT, Interface) outside the library block, is not explicitly or implicitly imported inside the library block, then this item is unknown (not known) to the clients of the type library.

Lets make a simple test. Save the VB project, and then close it. Otherwise the UDTDemo project will not pass the link step. Inside the "UDTDemo.idl" file go inside the library block and add the following lines.

//UDTDemo.IDL

 

library UDTDemo   //UDTDEMOLib

{

    importlib("stdole32.tlb");

    importlib("stdole2.tlb");

   

    struct UDTVariable;

    struct UDTArray;

   

};

Build the UDTDemo project once more and open the VB demo project. Open the references dialog, uncheck the UDTDemo line close it and then register our UDTDemo again with the VB project through the references.

Opening the object browser now, will show both the UDT's we have defined in our library. Close the VB project, and comment out the previous lines in the "UDTDemo.idl" file. These structures will be added implicitly into the library through the interfaces we are going to define.

End of the test.

The big secret for our UDT is that the MIDL compiler attaches enough information about it with the library, so as it may be described with the IRecordInfo interface. So, Ole Automation marshaling knows how to use our UDT type as a VT_RECORD type. Items identified as records may be wired. So do arrays of records.

One more thing. The SAFEARRAY(UDTVariable) declaration is a typedef for LPSAFEARRAY. This means that the structure is really declared as

 

    struct UDTArray

    {

        LPSAFEARRAY NamedVars;

    }

 

This leads us to the conclusion that there is no information provided for us about the type of data the array holds inside our code. Only type library compitible clients know the type information.

The Demo UDT Object

So far we have some really useless structures. We may not use these anywhere, except in VB internally only if we change the "UDTDemo.idl" file.

So to make our demo project a bit useful, lets add an object to our project. Use the hopefully well known insert "new ATL Object" menu item. In the Atl Object Wizard select "simple object" and press "next".


Then type "UDTDemoOb" as a short name in the "ATL object wizard properties". We may use what ever name we like, but we have to avoid using "UDTDemo" as it collides with the library name.

Then as I may always suggest, in the attributes tab, check the "support IsupportErrorInfo" choice, leave it apartment threaded, but as it dawned on me right now, check the "Support Connection points" on the dialog as well.



Pressing "ok" now the wizard will create two interfaces and a coclass object for as in the IDL file, and a class to implement the UDTDemoOb interface.

We checked the support for connection points, because when we use the proxy code generator for connection point interfaces, the code is far from right in the first place, when any of the parameters is of array type. It gives a slight warning about booleans, and compiles the wrong code. So we have to see it as well.

At this point, as It is mentioned at the beginning of this document, I am going to replace the wizard generated UUIDs. the rest of you may compile the project or check this with me.

You may skip this if you like

Do not compile the project yet.

First copy the library UUID and paste it above every UUID defined for a) the IUDTDemoOb interface, b) the _IUDTDemoObEvents events interface and c) the UDTDemo coclass. While you copy the UUID, you may comment out the wizard generated ones. Then starting with the above stated order increase by one the first part of the library interface, for each new occurrence. Parts of the code will look like this.

Collapse

//UDTDemo.idl

 

    [

        object,

        //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F), //previous one

        uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F), //library one, modified

        dual,

        helpstring("IUDTDemoOb Interface"),

        pointer_default(unique)

    ]

    interface IUDTDemoOb : IDispatch

    {

    };

 

    [

        //uuid(9117A523-34C3-11D4-A13A-AAA07458B90F), //previous one

        uuid(C21871A3-33EB-11D4-A13A-BE2573A1120F), //library one, modified

        helpstring("_IUDTDemoObEvents Interface")

    ]

    dispinterface _IUDTDemoObEvents

    {

        properties:

        methods:

    };

 

    [

        //uuid(9117A522-34C3-11D4-A13A-AAA07458B90F), //previous one

        uuid(C21871A4-33EB-11D4-A13A-BE2573A1120F), //library one, modified

        helpstring("UDTDemoOb Class")

    ]

    coclass UDTDemoOb

    {

        [default] interface IUDTDemoOb;

        [default, source] dispinterface _IUDTDemoObEvents;

    };

In the above items you may notice that the newly created UUIDs defer in the first part, and they are consequent. But these defer in both the first and second part with the UUID of the library. The fact is that these UUIDs are created one day later, than the one created for the library.

Since the newly created uuid's are consequent we know we are not mistaken to replace them with others consequent also, which should have been created in the past.

At this moment there are three more occurrences of the UUID of the coclass UDTDemo object. These are in the "UDTDemo.rgs" file. So copy the new UUID of the object, open the ".rgs" file in the editor, and replace the old UUID with the new one.

The above are performed for all objects created by the wizard.

Collapse

// UDTDemoOb.rgs

 

HKCR

{

    UDTDemo.UDTDemoOb.1 = s 'UDTDemoOb Class'

    {

        CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F}'

    }

    UDTDemo.UDTDemoOb = s 'UDTDemoOb Class'

    {

        CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F}'

        CurVer = s 'UDTDemo.UDTDemoOb.1'

    }

    NoRemove CLSID

    {

        ForceRemove { CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F } = s 'UDTDemoOb Class'

        {

            ProgID = s 'UDTDemo.UDTDemoOb.1'

            VersionIndependentProgID = s 'UDTDemo.UDTDemoOb'

            ForceRemove 'Programmable'

            InprocServer32 = s '%MODULE%'

            {

                val ThreadingModel = s 'Apartment'

            }

            'TypeLib' = s '{C21871A1-33EB-11D4-A13A-BE2573A1120F}'

        }

    }

}

End of skip area

Compile the project. Make sure everything is ok. If we check the project with the VB client at this point, we will only see the UDTDemo object appear in the object browser. This is correct.

So lets go on and add a property to our object. Using the property wizard add a property named UdtVar, accepting a pointer to a UDTVariable. We'll get later to the pointer thing. The UDTVariable is not in the type list of the dialog, so we have to manually add it. Check the picture bellow.


This is how our interface looks like after pressing the [Ok] button.

    [

        object,

        //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F),

        uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F),

        dual,

        helpstring("IUDTDemoOb Interface"),

        pointer_default(unique)

    ]

    interface IUDTDemoOb : IDispatch

    {

        [propget, id(1), helpstring("Returns a UDT Variable in the UDTObject")]

                HRESULT UdtVar([out, retval] UDTVariable * *pVal);

        [propput, id(1), helpstring("Sets a UDT Variable in the UDTObject")]

                HRESULT UdtVar([in] UDTVariable * newVal);  

    };

Lets check the put property first. Most of us know that in the put property we have to pass variables "by value". Here we defined a [in] variable as pointer to UDTVariable. So we pass the variable "by reference". In the C and C++ field we know that this is faster to do so. The same applies to VB and COM. In VB when dealing with types and structures we are forsed to use the byref declaration, no matter which direction the data goes to. It is up to the callee to enforce the integrity of the incoming data, so that when the method returns the input parameter is unchanged.

On the other hand the get property takes an argument of type pointer to pointer. In the beginning it looks right, since a "pointer to pointer" is a reference to a "pointer", and the get property argument type is always declared as the pointer to the put property argument type.

As always when the argument is an out one, the callee is responsible for allocating the memory. This means that we have to call "new UDTVariable" in our get_ method. But VB does not understand pointers. Does it?.


The above VB error says that VB can not accept a pointer to pointer in a get method returning a UDT. So we have to alter the get property of our object to accept only a pointer to UDTVariable. Still our method handles the memory allocation for the extra string in the UDT. Let's see it.

VB dimension a UDTVariable

         Allocate memory for the UDT.

         The memory is sizeof( UDTVariable ).

         Pass the variables address to the object.

                 Object allocates memory for UDT.Name

                 Object initializes the string

                 If object.special is not an integral type

                          allocates memory for the type

                 set the value of Object.Special.

So our get method is still responsible for allocating memory for the UDTVariable. It just does not allocate the UDTVariable body memory.

So after this we may go to the get method of our interface, and remove one of the "*" characters. Alongside with this modification change the argument names from pVal and newVal to "pUDT". This is a bit more clear for the VB, C++ client app developer since the beginning of autocompletion in the studio environment.

We also want this property to be the default one. Go and replace the id(1) with id(0) in both methods. Our interface now looks like this.

    [

        object,

        //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F),

        uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F),

        dual,

        helpstring("IUDTDemoOb Interface"),

        pointer_default(unique)

    ]

    interface IUDTDemoOb : IDispatch

    {

        [propget, id(0), helpstring("Returns a UDT Variable in the UDTObject")]

                HRESULT UdtVar([out, retval]       

                   UDTVariable *pUDT);

        [propput, id(0), helpstring("Sets a UDT Variable in the UDTObject")]

                HRESULT UdtVar([in]      

                  UDTVariable *pUDT);

    };

This is not enough though. We have to inform the CUDTDemoOb class for the change in the interface. So go to the header file, remove the "*" from the get_UdtVar method, and since we are there change the name of the argument to "pUDT". Do the same for the ".cpp" file.

Here are the modifications in the CUDTDemoOb class

//CUDTDemoOb.h

 

    STDMETHOD(get_UdtVar)(/*[out, retval]*/ UDTVariable *pUDT);

    STDMETHOD(put_UdtVar)(/*[in]*/  UDTVariable *pUDT);

 

//CUDTDemoOb.cpp

 

    STDMETHODIMP CUDTDemoOb::get_UdtVar(UDTVariable *pUDT)

    STDMETHODIMP CUDTDemoOb::put_UdtVar(UDTVariable *pUDT)

We are now ready to compile the project.

So what are these warnings about incompatible automation interface?. ("warning MIDL2039 : interface does not conform to [oleautomation] attribute") You may safely ignore this warning. It is stated in several articles. When the MIDL compiler is upgraded the warning will go away. (well, this may not be right in case the interface is defined inside the Library block).

We may open the VB project again, and check the object browser. The property is there, declared for our object. There is also a reference to the UDTVariable. This is correct, since now the UDT is implicitly inserted into the UDTDemo library through the IUDTDemoOb interface.

Using the UDTVariable

Lets go back to the UDTDemo library and make it do something useful. First we need a UDTVariable member in the CUDTDemoOb class. So open the header file and add a declaration for a variable.

//CUDTDemoOb.h

 

protected:

    UDTVariable m_pUDT;

 

We also have to modify the constructor of our class to initialize the m_pUDT structure. We also need to add a destructor to the class.

//CUDTDemoOb.h

 

CUDTDemoOb()

{

    CComBSTR str = _T("Unnamed");

    m_pUDT.Value = 0; //default value zero (0)

    m_pUDT.Name = ::SysAllocString( str ); //default name "Unnamed"

    ::VariantInit( &m_pUDT.Special ); //default special value "Empty"

}

 

virtual ~CUDTDemoOb()

{

    m_pUDT.Value = 0; //just in case

    ::SysFreeString( m_pUDT.Name ); //free the string memory

    ::VariantClear( &m_pUDT.Special ); // free the variant memory

}

Now it is time we added some functionality into the properties of our class.

Always check for an incoming NULL pointer, when there is a pointer involved. So go into both the get_ and put_ properties implementation and add the following.

//CUDTDemoOb.cpp

If( !pUDT )

    return( E_POINTER );

Now get into the put_UdtVar property method. What we have to do, is assign the members of the incoming variable into the one our object holds. This is easy for the Value member but for the other two, we have to free their allocated memory before assigning the new values. That is why we have selected a string and a variant. So the code will now look like the following.

//CUDTDemoOb.cpp

 

STDMETHODIMP CUDTDemoOb::put_UdtVar(UDTVariable *pUDT)

{

    if( !pUDT )

        return( E_POINTER );

    if( !pUDT->Name )

        return( E_POINTER );

 

    m_pUDT.Value = pUDT->Value;  //easy assignment

 

    ::SysFreeString( m_pUDT.Name );  //free the previous string first

    m_pUDT.Name = ::SysAllocString( pUDT->Name );   //make a copy of the incoming

 

    ::VariantClear( &m_pUDT.Special );   //free the previous variant first

    ::VariantCopy( &m_pUDT.Special, &pUDT->Special );   //make a copy

 

    return S_OK;

}

As every great writer says, we remove error checks for clarity :).

You may have noticed that we also check the string Name for null value. We have to. BSTRs are declared as pointers so this field might be NULL. The point is that a NULL pointer is not an empty COM string. An Empty com string is one with zero length.

After the method returns, our object has a copy of the incoming structure, and that is what we wanted to do.

Now forward to the get_UdtVar method. This is the opposite of the previous one. We have to fill in the incoming structure with the values of the internal UDT structure of the object.

We may check the code.

//CUDTDemoOb.cpp

 

STDMETHODIMP CUDTDemoOb::get_UdtVar(UDTVariable *pUDT)

{

    if( !pUDT )

        return( E_POINTER );

 

    pUDT->Value = m_pUDT.Value;  //return value

 

    ::SysFreeString( pUDT->Name );   //free old (previous) name

     pUDT->Name = ::SysAllocString( m_pUDT.Name );  //copy new name

 

    ::VariantClear( &pUDT->Special );  //free old special value

    ::VariantCopy( &pUDT->Special, &m_pUDT.Special ); //copy new special value

 

    return  S_OK;

}

The main difference is now that the Name and Special members of the incoming UDT may be NULL and Empty respectively. This is allowed because our object is obliged to fill in the structure. The callee is only responsible for allocating the memory for the UDT itself alone and not its members.

Why do we free the incoming string ?. well, because the callee may pass in an already initialized UDT. The SysFreeString and VariantClear system methods may handle NULL string pointers and empty variants respectively. Freeing the string may give us errors. In case the method is not called from VB the Name BSTR pointer, may hold a not NULL but invalid pointer (trash). So this would have been

HRESULT hr = ::SysFreeString( pUDT->Name ); //free old (previous) name

if( FAILED( hr ) )

    return( hr );  //if for any reason there is error FAIL

Compile the project, open the VB client project, add a button to the form, and do some checks with assignments there.

Private Sub cmdFirstTest_Click()

 

    Dim a_udt As UDTVariable  ''define a couple UDTVariables

    Dim b_udt As UDTVariable

   

    Dim ob_udt As New UDTDemoOb ''declare and create a UDEDemoOb object   

   

    a_udt.Name = "Ioannis"  ''initialize one of the UDTS

    a_udt.Value = 10

    a_udt.Special = 15.5

   

    ob_udt.UdtVar = a_udt  ''assign the initialized UDT to the object   

    b_udt = ob_udt.UdtVar   ''assign the UDT of the object to the second UDT

    ''put a breakpoint here and check the result in the immediate window

   

 End Sub             

Now try this.

    b_udt = ob_udt.UdtVar   ''assign the UDT of the object to the second UDT

        ''put a breakpoint here and check the result in the immediate window

 

    b_udt.Special = b_udt ''it actually makes a full copy of the b_udt

    b_udt.Special.Special.Name = "kostas" ''vb does not use references

ARRAYS OF UDTs

So, we have not seen any arrays so far you may say. It is our next step. We are going to add a method to our interface, which will return an array of UDTs. It will take two numbers as input, start and length, and will return an array of UDTVariables with length items, holding consequent values.

So go to the UDTDemo project, right click on the IUDTDemoOb interface, and select "add method".

In the Dialog, type "UDTSequence" as the name of the method, and add the following as the parameters. "[in] long start, [in] long length, [out, retval] SAFEARRAY(UDTVariable) *SequenceArr". Press [Ok] and lets see what the wizard added into the project for us.

Do not compile now !

Well the definition of the new method has been inserted into the IUDTDemoOb interface.

Collapse

//udtdemo.idl

 

    [

        object,

        //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F),

        uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F),

        dual,

        helpstring("IUDTDemoOb Interface"),

        pointer_default(unique)

    ]

    interface IUDTDemoOb : IDispatch

    {

        [propget, id(0), helpstring("Returns a UDT Variable in the UDTObject")]

                HRESULT UdtVar([out, retval] UDTVariable *pUDT);

        [propput, id(0), helpstring("Sets a UDT Variable in the UDTObject")]

                HRESULT UdtVar([in] UDTVariable *pUDT);

        [id(1), helpstring("Return successive named values")]

            HRESULT UDTSequence([in] long start

                                [in] long length,

                                [out, retval] SAFEARRAY(UDTVariable) *SequenceArr);

                              

    };

The above is edited a bit so it may be visible here at once. There should not be something we do not know so far. We saw earlier what SAFEARRAY(UDTVariable) is. This is the declaration of a pointer to a SAFEARRAY structure holding UDTVariables. So SequenceArr is really a reference to a SAFEARRAY pointer. Everything is fine so far.

Now lets check the header file of the CUDTDemoOb class.

//udtdemoob.h

 

public:

    STDMETHOD(UDTSequence)(/*[in]*/ long start,

                           /*[in]*/ long length,

                           /*[out, retval]*/ SAFEARRAY(UDTVariable) *SequenceArr);

 

STDMETHOD(get_UdtVar)(/*[out, retval]*/ UDTVariable *pUDT);

STDMETHOD(put_UdtVar)(/*[in]*/ UDTVariable *pUDT);

At first it looks right. It is not. There is not any macro or something visible to the compiler to understand the SAFEARRAY(UDTVariable) declaration. As we said at the beginning of this document, our code will never have enough type information about the SAFEARRAY structure. The type information for arrays should be checked at run time. So we have to modify the code. Replace SAFEARRAY(UDTVariable) with SAFEARRAY *.

This is how the code should look like.

//udtdemoob.h

 

public:

    STDMETHOD(UDTSequence)(/*[in]*/ long start,

                           /*[in]*/ long length,

                           /*[out, retval]*/ SAFEARRAY **SequenceArr);

 

STDMETHOD(get_UdtVar)(/*[out, retval]*/ UDTVariable *pUDT);

STDMETHOD(put_UdtVar)(/*[in]*/ UDTVariable *pUDT);

You've probably realized that we have to modify the implementation file of CUDTDemoOb class to correct this problem. Well I was surprised to see that for the first time, the wizard had not even added the declaration of the SequenceArr.

//udtdemoob.cpp

 

STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length )  //Where is the SafeArray ?

{

    return S_OK;

}

As you see, we have to add the SAFEARRAY **SequenceArr declaration. On the other hand if the SequenceArr was declared just replace is as we did in the header.

//udtdemoob.cpp

 

STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length, SAFAARRAY **SequenceArr )

{

    return S_OK;

}

Now we may compile the project. Check again the VB client project, in the object browser to see the new method, and that it returns an array of UDTVarables.


 

So return to the implementation of UDTSequence to start adding checking code. First we have to test that the outgoing array variable is not null. The second check is the length variable. It may not be less than or equal to zero.

//udtdemoob.cpp

 

STDMETHODIMP CUDTDemoOb::UDTSequence(long start, longlength,

                                     SAFEARRAY **SequenceArr)

{

    if( !SequenceArr )

        return( E_POINTER );

 

    if( length <= 0 ) {

        HRESULT hr=Error(_T("Length must be greater than zero") );

        return( hr );

    }

   

    return S_OK;

}

You may notice the usage of the Error method. This is provided by ATL and is very easy to notify clients for errors without getting into much trouble.

The next step is to check the actual array pointer. The dereferenced one. This is the "*SequenceArr". There are two possibilities at this point. Ether this is NULL, which is Ok since we return the array, or holds some non zero value, where supposing it is an array we clear it and create a new one.

So the method goes on.

//udtdemoob.cpp

 

STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length,

                                     SAFEARRAY **SequenceArr)

{

    if( !SequenceArr )

        return( E_POINTER );

 

    if( length <= 0 ) {

        HRESULT hr = Error( _T("Length must be greater than zero") );

        return( hr );

    }

 

    if( *SequenceArr != NULL ) {

        ::SafeArrayDestroy( *SequenceArr );

        *SequenceArr = NULL;

    }

   

    return S_OK;

}

Create The Array

Now we may create a new array to hold the sequence of named variables. Our first thought here is to use the ::SafeArrayCreate method, since we do not know what we exactly need. Search the MSDN library and in the documentation we find nothing about UDTs. On the other hand the ::SafeArrayCreateEx method implies it may create an array of Records (UDTs).

As the normal version, this method needs access to a SAFEARRAYBOUND structure, the number of dimensions, the data type, and a pointer to IRecordInfo interface. So, go by the book. a) we need an array of "records" use VT_RECORD, b) we need only one (1) dimension, c) we need a zero based array (lbound) with length (cbElements). Ok. This is what we have so far.

SAFEARRAYBOUND rgsabound[1];

rgsabound[0].lLbound = 0;

rgsabound[0].cElements = length;

*SequenceArr = ::SafeArrayCreateEx(VT_RECORD, 1, rgsabound, /*what next ?*/ );

Searching in the MSDN once more, reveals the "::GetRecordInfoFromGuids" method. Actually there are two of them, but this one seemed easier to use for this tutorial. The arguments to this method are,:

  • rGuidTypeLib : The GUID of the type library containing the UDT. In our case UDTDemo library, LIBIID_UDTDemo

  • uVerMajor : The major version number of the type library of the UDT. The version of this library is (1.0). so major version is 1.

  • uVerMinor : The minor version number of the type library of the UDT. Zero (0) in our case

  • lcid : The locale ID of the caller. Usually zero is a default value. Use zero.

  • rGuidTypeInfo : The GUID of the typeinfo that describes the UDT. This is the GUID of UDTVariable, but it is not found anywhere.

  • ppRecInfo : Points to the pointer of the IRecordInfo interface on a RecordInfo object. This pointer we pass to the "::SafeArrayCreateEx" method.

So, go into the IDL file, copy the uuid of the UDTVariable structure and paste it at the beginning of the implementation file. Then make it a formal UUID structure.

So this "C21871A0-33EB-11D4-A13A-BE2573A1120F" becomes

//udtdemoob.cpp

 

const IID UDTVariable_IID = { 0xC21871A0,

                              0x33EB,

                              0x11D4, {

                                       0xA1,

                                       0x3A,

                                       0xBE,

                                       0x25,

                                       0x73,

                                       0xA1,

                                       0x12,

                                       0x0F

                                      }

                            };  

now we are ready, to create an uninitialized array of UDTVariable structures. inside the UDTSequence function

Collapse

    //////////////////////////////////////////////////

    //here starts the actual creation of the array

    //////////////////////////////////////////////////

    IRecordInfo *pUdtRecordInfo = NULL;

    HRESULT hr = GetRecordInfoFromGuids( LIBID_UDTDemo, 

                                         1, 0,

                                         0,

                                         UDTVariable_IID,

                                         &pUdtRecordInfo );

    if( FAILED( hr ) ) {

        HRESULT hr2 = Error( _T("Can not create RecordInfo interface for"

                                "UDTVariable") );

        return( hr ); //Return original HRESULT hr2 is for debug only

    }

   

    SAFEARRAYBOUND rgsabound[1];

    rgsabound[0].lLbound = 0;

    rgsabound[0].cElements =length;

 

    *SequenceArr = ::SafeArrayCreateEx( VT_RECORD, 1, rgsabound, pUdtRecordInfo );

 

    pUdtRecordInfo->Release(); //do not forget to release the interface

    if( *SequenceArr == NULL ) {

        HRESULT hr = Error( _T("Can not create array of UDTVariable "

                               "structures") );

        return( hr );

    }

    //////////////////////////////////////////////////

    //the array has been created

    //////////////////////////////////////////////////

Now we have created an uninitialized array, and have to put data on it. You may also make tests with VB at this point, to check that the method returns arrays with the expected size. Even without data.

If you get an the HRESULT error code "Element not found" make sure you have typed the UDTVariable_IID correctly.

At this point you should also know that the memory which has been allocated by the system for the array is zero initialized. This means that the Value and Name members are initialized to zero (0) and the Special member is initialized to VT_EMPTY. This is helpful in case we'd like to distinguish between an initialized or not slot in the array.

Add Data into the Array

There are two ways to fill in an array with data. One is to add it one by one, using the ::SafeArrayPutElement method, and the other is to use the ::SafeArrayAccessData to manipulate the data a bit faster. In my experience we are going to use the first one when we want to access a single element and the second one when we need to perform calculation in the whole range of the data the array holds.

Safe arrays of structures appear in memory as normal arrays of structures. At first there might be a misunderstanding that in the SAFEARRAY there is record information kept with every item in the array. This is not true. There is only one IRecordInfo or ITypeInfo pointer for the whole array. SAFEARRAYs use a simple old trick. They allocate the memory to hold the SAFEARRAY structure but there is also some more memory allocated to hold the extra pointer if necessary at the begining. This is stated in the MSDN library.

So now we are going to create two internal methods for demonstrating both ways of entering data into the array.

First we'll use the ::SafeArrayPutElement method. In the CUDTDemoOb class declaration, insert the declaration of this method. This method should be declared protected, since it will only be called internally by the class itself.

//udtdemoob.h

 

protected:

    HRESULT SequenceByElement(long start, long length, SAFEARRAY *SequenceArr);

 

The only difference from the UDTSequence method is that this one accepts only a pointer to a SAFEARRAY. Not the pointer to pointer used in SAFEARRAY (UDTSequence).

The algorithm to fill the array is really simple. For every UDTVariable in the array, we set successive values starting from start into the Value member of our structure, convert this numerical to BSTR and assign it to the Name member of the structure. Finally set the value of the Special member to be either of type long or double and assign to it the same numeric value, except that when we use the double version add "0.5" to have different data there.

In the implementation file of our class add the method definition.

//udtdemoob.cpp

 

HRESULT CUDTDemoOb::SequenceByElement(long start,

                                      long length,

                                      SAFEARRAY *SequenceArr)

{

    return( S_OK );

}

we may skip checking the incoming variables in this method, since these are supposed to be called only inside the class, and the initial checks taken before calling these.

//udtdemoob.cpp

 

HRESULT CUDTDemoOb::SequenceByElement(long start,

                                      long length,

                                      SAFEARRAY *SequenceArr)

{

    HRESULT hr = SafeArrayGetLBound( SequenceArr, 1, &lBound );

    if( FAILED( hr ) ) return( hr );

 

    return( S_OK );

}

The first check to be performed is the lower bound of the array. Although we state that we handle zero-based arrays, one may pass a special bounded array. In VB it is easy to get one-based arrays. It is also a way to know we have a valid SAFEARRAY pointer.

The following code makes the conversion from numeric to string, and assigns the string value to the Name member of the a_udt structure.

//udtdemoob.cpp

 

HRESULT CUDTDemoOb::SequenceByElement(long start,

                                      long length,

                                      SAFEARRAY *SequenceArr)

{

    HRESULT hr = SafeArrayGetLBound( SequenceArr, 1, &lBound );

    if( FAILED( hr ) )

        return( hr );

 

    hr = ::VariantChangeType( &a_variant, &a_udt.Special, 0, VT_BSTR );

    hr = ::VarBstrCat( strDefPart, a_variant.bstrVal, &a_udt.Name );

 

    return( S_OK );

}

You may see the code in the accompanying project, so we are going to explain the big picture. Inside the loop this line is executed.

//udtdemoob.cpp

 

HRESULT CUDTDemoOb::SequenceByElement(long start,

                                      long length,

                                      SAFEARRAY *SequenceArr)

{

    HRESULT hr = SafeArrayGetLBound( SequenceArr, 1, &lBound );

    if( FAILED( hr ) )

        return( hr );

 

    hr = ::VariantChangeType( &a_variant, &a_udt.Special, 0, VT_BSTR );

    hr = ::VarBstrCat( strDefPart, a_variant.bstrVal, &a_udt.Name );

 

    hr = ::SafeArrayPutElement( SequenceArr, &i, (void*)&a_udt );

 

    return( S_OK );

}

In this line of code, the system adds the a_udt in the ith position of the array. What we have to know is that in this call, the system makes a full copy of the structure we pass in it. The reason the system may perform the full copy is the usage of the IRecordInfo interface we used in the creation of the array. As a result we have to release the memory held by any BSTR or VARIANT we use. In our situation we only release the a_variant variable since this holds the reference of the only resource allocated string.

Let's move to the ::SafeArrayAccessData method and check out the differences. The first change, is that now we use a pointer to UDTVariable p_udt. The second big difference is that inside the loop there is only code to set the members of the structure, through the pointer. The only actual code to access the array is outside the loop with the methods to access and release the actual memory the data resides to. There is also one more check inside the loop

//udtdemoob.cpp

    //....

    if( p_udt->Name )

        ::SysFreeString( p_udt->Name );

    //....

This is to demonstrate that since we access the data without any other interference we have to release any memory allocated for a BSTR string, a VARIANT or even an interface pointer before assigning data to it. As it was pointed before, checking for the NULL value might be adequate for this simple demonstration.

I hope it is obvious that it is better calling the second method - ::SafeArrayAccessData - when there is need to access all or most of the data in the array, but might also be appropriate to use the the ::SafeArrayGetElement and ::SafeArrayPutElement pair of methods if you want to modify one or two elements at a time.

As a final step insert the following lines at the end of the body of the UDTSequence method, and test it with the VB client project. You may comment out which ever you like to see how it works, and that they both give the same results.

//   hr = SequenceByElement( start, length, *SequenceArr );

     hr = SequenceByData( start, length, *SequenceArr );

Static Arrays

Our method presents a fault in the design. It may only return a dynamically created array. This means the array is created on the heap. Try adding the following lines in VB and check this out.

    dim a_udt_arr(5) as UDTVariable

    dim a_udt_ob     as UDTDemoOb

 

    a_udt_arr = UDTDemoOb.UDTSequence(15, 5) ''Error here

Well, conformant arrays, I think this is what they call them, are only available as [in] arguments in this demo. So for the moment add one more check to our UDTSequence method. The other problem is that arrays are always passed as doubly referenced pointers.

So let's try out a modify the array approach.

Add one more property to the interface

Call it Item like in collections. The signature will be

//udtdemoob.idl

 

    [propput, .....]

    Item( [in] long index,

          [in] SAFEARRAY(UDTVariable) *pUDTArr,

          [in] UDTVariable *pUDT );

    [propget, .....]

    Item( [in] long index,

          [in] SAFEARRAY(UDTVariable) *pUDTArr,

          [out, retval] UDTVariable *pUDT );

The reason we add this, is to demonstrate some checks for the incoming arrays. As you may have guessed by the method definition, arrays although defined as [in] are still modifiable in every way. Our first check is to see if it is an array of UDTVariable structures. Since this check is performed in at least two methods, we may put it in its own protected function inside the object implementation class.

As you have noticed, our object still does not keep any state about the incoming arrays.

HRESULT IsUDTVariableArray( SAFEARRAY *pUDTArr, bool &isDynamic )

The only difference in what you might expect is the bool reference at the end of the declaration. Well, this check function will be able to inform us if a) we may actually modify the array, (append or remove items by reallocating the memory, or even destroy and recreate the array), b) we may only modify individual UDTVariable structures inserted in the array. The former feature will not be implemented in the demonstrating project.

Our first check is the number of dimensions of the incoming array. We want this to be one dimensioned. After reading the tutorial you may expand this to multidimensional arrays although there is a slight issue.

long dims = SafeArrayGetDim( pUDTArr );

if( dims != 1 ) {

    hr = Error( _T("Not Implemented for multidimentional arrays") );

    return( hr ); 

}

the next step is to check that the array is created so as to hold structures. This is easily done by checking that the features flag of the incoming array indicates records support.

unsigned short feats = pUDTArr->fFeatures; //== 0x0020;

if( (feats & FADF_RECORD) != FADF_RECORD ) {

    hr = Error( _T("Array is expected to hold structures") );

    return( hr );

}

Final check is to compare the name of the structure the array holds with ours. To do this we have to get access to the IRecordInfo interface pointer the array holds.

IRecordInfo *pUDTRecInfo = NULL;

hr = ::SafeArrayGetRecordInfo( pUDTArr, &pUDTRecInfo );

if( FAILED( hr ) &&  !pUDTRecInfo )

    return( hr );

Now do the comparing.

BSTR  udtName = ::SysAllocString( L"UDTVariable" );

BSTR  bstrUDTName = NULL; //if not null. we are going to have problem

hr = pUDTRecInfo->GetName( &bstrUDTName);

if( VarBstrCmp( udtName, bstrUDTName, 0, GetUserDefaultLCID()) != VARCMP_EQ ) {

    ::SysFreeString( bstrUDTName );

    ::SysFreeString( udtName );

    hr = Error(_T("Object Does Only support [UDTVariable] Structures") );

   

    return( hr );

}

In the accompanying project there are also some more checks as demonstration, which are available only through the debugger. Implementing the Item property is straightforward after this.

Using VARIANTS

I do not think this is enough so far, as we have not discussed using our structure with variants. So let's add one more property to our object. Add the following definition to our interface.

HRESULT VarItem([in] long items, [out, retval]

                 LPVARIANT pUdtData );

Now go to the definition of the new property in the implementation file of the CUDTDemoOb class and let's do something.

First some checks. The usual check for the null pointer, and then check if the VARIANT contains any data. If it is not empty we should clear it.

    if( !pUdtData )

        return( E_POINTER );

 

    if( pUdtData->vt != VT_EMPTY )

            ::VariantClear( pUdtData );

The next step is to implement the algorithm which is to return a) a single UDTVariable structure if the item variable is equal or less than one (1). b) an array of structures if item is larger than one (1).

In both situations we have to set the type of the outgoing VARIANT to VT_RECORD, and this is the only similarity in accessing the VARIANT pUdtData variable. For the single UDTVariable structure, we have to set the pRecInfo member of the VARIANT to a valid IRecordInfo interface pointer. This has been demonstrated earlier. Then assign the new structure to the pvRecord member of the variant. Returning an array on the other hand, we must update the type of the outgoing VARIANT to be of type VT_ARRAY as well. Then we just assign an already constructed array to the parray member of the variant. Both the assignments are easily done, since we have already implemented appropriate properties and methods in our object.

Collapse

if( items <= 1 ) {

    IRecordInfo *pUdtRecordInfo = NULL;

    hr = ::GetRecordInfoFromGuids( LIBID_UDTDemo,

                                   1, 0,

                                   0,

                                   UDTVariable_IID,

                                   &pUdtRecordInfo );

    if( FAILED( hr ) ) {

        HRESULT hr2= Error( _T("Can not create RecordInfo"

                            "interface for UDTVariable") );

        return( hr );

    } //assign record information on the variant

 

    pUdtData->pRecInfo = pUdtRecordInfo;

    pUdtRecordInfo = NULL;  //MIND. we do not release the interface.

                            //VariantClear should

   

    pUdtData->vt = VT_RECORD;

    pUdtData->pvRecord= NULL;

    hr= get_UdtVar( (UDTVariable*) &(pUdtData->pvRecord) );

 

} else  {

 

    //here the valid pointer of the union is the array.

    //so the array holds the record info.

 

    pUdtData->vt = VT_RECORD | VT_ARRAY;

    hr = UDTSequence(1, items, &(pUdtData->parray) );

}

I think this is enough for a basic tutorial on UDT's with COM. There is no interface defined to access the second type UDTArray defined in the type library, but this should be straightforward at this moment (I tricked you :) ). In the demo project, I've explicitly added the structure in the library body, so you can play with this in VB.

"Safe Arrays" in EVENTS

I've also said that there is a flaw in the code created by the wizard for the interfaces creates to pass any kind of arrays back. This is partially been taken care of with the implementation of the VarItem method. An event method is demonstrated in the project. Here is what has been changed in the generated method.

Supposing that not many of us have used events in the controls, I am going to be a bit more specific on this.

Let's begin the journey to ConnectionPoints. First we have to add a method to the IUDTDemoObEvents interface. Here is the signature of this method. So far you have the knowledge to understand the signature of this method. Additionally only the UDTDemo.idl has changed so far.

[id(1), helpstring

 ("Informs about changes in an array of named vars")]

     HRESULT ChangedVars(SAFEARRAY(UDTVariable) *pVars);


Now compile once more the project, and check the Object Browser in the VB client. You may see the event declared in the object.


Now where the project is compiled, and the UDTDemo type library is updated, we may update the CUDTDemoOb class to use the IUDTDemoObEvents interface. In the project window, right click on the CUDTDemoOb class, and from the popup menu select Implement connection point.


In the following dialog box, select the (check on it) _IUDTDemoObEvents interface and press [ok].


The wizard has now added one more file into the project. "UDTDemoCP.h" in which the CProxy_IUDTDemoEvents< class T > template class is implemented, and handles the event interface of the UDTDemoOb coclass object. The CUDTDemoOb class is now deriving from the newly generated proxy class.

The proxy class holds the Fire_ChangedVars method, which is implemented and we can call it from any point of our class to fire the event.

So let's go to the implementation of the UDTSequence method just for the demonstration and fire the event.

//UDTDemoOb.cpp  - UDTSequence method

 

    //hr = SequenceByElement( start, length, *SequenceArr );

    hr =  SequenceByData( start, length, *SequenceArr);

   

    return Fire_ChangedVars( SequenceArr  );

    //<<----  changed here //return S_OK;     

Now compile the project, and watch the output.

    warning C4800:

    'struct tagSAFEARRAY ** ' : forcing value to bool 'true' or 'false'

    (performance warning)

This is not really a warning. This is an implementation error and causes runtime problems. Let's see just for the demonstration of it. Open the VB Client again and add the following in the declarations of the demo form. I hope you know what the WithEvents keyword means.

    Dim WithEvents main_UDT_ob As UDTDemoOb

Update the following as well

Private Sub Form_Load()

 

    Set main_UDT_ob = New UDTDemoOb

 

End Sub

 

 

Private Sub Form_Unload(Cancel As Integer)

 

    Set main_UDT_ob = Nothing

 

End Sub

 

Private Sub main_UDT_ob_ChangedVars(pVars() As UDTDemo.UDTVariable)

 

    Debug.Print pVars(1).Name, pVars(1).Special, pVars(1).Value

 

End Sub

Set a breakpoint in the debug statement of the event handler and run the client. See what we get.


And in stand alone execution we get


Well, the actual error is the following and should be the expected error since we know the warning. This was discovered in the VC++ debugger as the return HRESULT of the Invoke method.

0x80020005 == Type Mismatch

It's time we checked the code the wizards generated for us.

Collapse

HRESULT Fire_ChangedVars(SAFEARRAY * * pVars)

{

    CComVariant varResult;

    T* pT = static_cast<T*>(this);

    int nConnectionIndex;

    CComVariant* pvars = new CComVariant[1];

    int nConnections = m_vec.GetSize();

       

    for( nConnectionIndex = 0; nConnectionIndex < nconnections; nConnectionIndex++) {

        pT->Lock();

        CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);

        pT->Unlock();

        IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);

        if (pDispatch != NULL)

        {

            VariantClear(&varResult);

            pvars[0] = pVars;

            DISPPARAMS disp = { pvars, NULL, 1, 0 };

            pDispatch->Invoke( 0x1,

                               IID_NULL,

                               LOCALE_USER_DEFAULT,

                               DISPATCH_METHOD,

                               &disp,

                               &varResult,

                               NULL, NULL); 

        }

    }

    delete[] pvars;

    return varResult.scode;

}

lets check the trouble lines.

    CComVariant* pvars = new CComVariant[1];

    int nConnections = m_vec.GetSize();

This logically assumes that there might be more than one clients connected with the instance of our object. But no error check means that at least one client is expected to be connected. This is wizard code so it should perform some checks. We are not expected to know every detail of the IConnectionPointImpl ATL class.

    int nConnections = m_vec.GetSize();

    if( !nConnections )

        return S_OK;

 

    CComVariant* pvars = new CComVariant[1];

Of course I'm exaggerating, but this is my way of doing such things.

This final line, incorrectly assumes that there is only one client connected to our object. Each time Invoke is called inside the loop, the varResult variable is set to the return value of the method being invoked. Neither varResult is being checked for returning any error code, neither the return value of the Invoke method itself, which in our project gave the right error. So as is, calling the event method, will succeed or fail depending on notifying the last object connected with our UDTDemoOb object. Consider using a Single Instance Exe Server with clients connected on it !

    pDispatch->Invoke( 0x1, .. .

 

    return varResult.scode;

this is not to blame anyone, since if we'd like per connection error handling we should make it ourselves. Just remember that you have to take care of it depending on the project.

The Actual Problem

pvars[0] = pVars;

CComVariant does not handle arrays of any kind. But since it derives directly from the VARIANT structure it is easy to modify the code to do the right thing for us. We used VARIANTs earlier so you may try it yourselves first.

    //pvars[0] = pVars;

   

    pvars[0].vt = VT_ARRAY | VT_BYREF | VT_RECORD;

    pvars[0].pparray = pVars;

To pass any kind of array with a VARIANT you just have to define the VT_Type of the array, or'd with the VT_ARRAY type. The only difference from our previous example is that here we use the VT_BYREF argument as well. This is necessary since we have a pointer to pointer argument. Of course byref in VB means we use the "pparray" member of the variant union. For an array holding strings it would be

pvars[0].vt = VT_ARRAY | VT_BSTR; //array to strings

pvars[0].parray = ...

 

pvars[0].vt = VT_ARRAY | VT_BYREF | VT_BSTR; //pointer to array to strings

pvars[0].pparray = ...

Again, although we deal with an array holding UDT structures we do not have to set an IRecordInfo interface inside the variant.

Compile the project and try this out. Do not fear unless you change the idl file of the project the code does not change. This is the reason we first define all methods in the event (sink) interface and then implement the connection point interface in our object.

Final Note

As most of you may have noticed this has been written quite some time ago. The reason it is posted at this moment is that I had to use user defined structures (UDTs) for a demo project I work on, and this article was really helpful during its implementation. So I hope it is worth reading and helpful to the developer community as well.

References:

MSDN Library:

Platform SDK /Component Services / COM / Automation / User Defined Data Types. Extending Visual Basic with C++ DLLs, by Bruce McKinney. April 1996

MSJ magazine:

Q&A ActiveX / COM, by Don Box. MSJ November 1996 Underastanding Interface Definition Language: A Developer's survival guide, by Bill Hludzinski MSJ August 1998.

Books:

Beginning ATL COM Programming, by Richard Grimes, George Reilly, Alex Stockton, Julian Templeman, Wrox Press, ISBN 1861000111 Professional ATL COM Programming, by Richard Grimes. Wrox Press. ISBN 1861001401

<script src="/script/togglePre.js" type="text/javascript"> </script> ioannhs_s

 

Click here to view ioannhs_s's online profile.

 

 

 

一、              爲什麼要用COM

  軟件工程發展到今天,從一開始的結構化編程,到面向對象編程,再到現在的COM編程,目標只有一個,就是希望軟件能象積方塊一樣是累起來的,是組裝起來的,而不是一點點編出來的。結構化編程是函數塊的形式,通過把一個軟件劃分成許多模塊,每個模塊完成各自不同的功能,儘量做到高內聚低藕合,這已經是一個很好的開始,我們可以把不同的模塊分給不同的人去做,然後合到一塊,這已經有了組裝的概念了。軟件工程的核心就是要模塊化,最理想的情況就是100%內聚0%藕合。整個軟件的發展也都是朝着這個方向走的。結構化編程方式只是一個開始。下一步就出現了面向對象編程,它相對於面向功能的結構化方式是一個巨大的進步。我們知道整個自然界都是由各種各樣不同的事物組成的,事物之間存在着複雜的千絲萬縷的關係,而正是靠着事物之間的聯繫、交互作用,我們的世界纔是有生命力的纔是活動的。我們可以認爲在自然界中事物做爲一個概念,它是穩定的不變的,而事物之間的聯繫是多變的、運動的。事物應該是這個世界的本質所在。面向對象的着眼點就是事物,就是這種穩定的概念。每個事物都有其固有的屬性,都有其固有的行爲,這些都是事物本身所固有的東西,而面向對象的方法就是描述出這種穩定的東西。而面向功能的模塊化方法它的着眼點是事物之間的聯繫,它眼中看不到事物的概念它只注重功能,我們平常在劃分模塊的時侯有沒有想過這個函數與哪些對象有關呢?很少有人這麼想,一個函數它實現一種功能,這個功能必定與某些事物想聯繫,我們沒有去掌握事物本身而只考慮事物之間是怎麼相互作用而完成一個功能的。說白了,這叫本末倒置,也叫急功近利,因爲不是我們智慧不夠,只是因爲我們沒有多想一步。面向功能的結構化方法因爲它注意的只是事物之間的聯繫,而聯繫是多變的,事物本身可能不會發生大的變化,而聯繫則是很有可能發生改變的,聯繫一變,那就是另一個世界了,那就是另一種功能了。如果我們用面向對象的方法,我們就可以以不變應萬變,只要事先把事物用類描述好,我們要改變的只是把這些類聯繫起來的方法,只是重新使用我們的類庫,而面向過程的方法因爲它構造的是一個不穩定的世界,所以一點小小的變化也可能導致整個系統都要改變。然而面向對象方法仍然有問題,問題在於重用的方法。搭積木式的軟件構造方法的基礎是有許許多多各種各樣的可重用的部件、模塊。我們首先想到的是類庫,因爲我們用面向對象的方法產生的直接結果就是許多的類。但類庫的重用是基於源碼的方式,這是它的重大缺陷。首先它限制了編程語言,你的類庫總是用一種語言寫的吧,那你就不能拿到別的語言裏用了。其次你每次都必須重新編譯,只有編譯了才能與你自己的代碼結合在一起生成可執行文件。在開發時這倒沒什麼,關鍵在於開發完成後,你的EXE都已經生成好了,如果這時侯你的類庫提供廠商告訴你他們又做好了一個新的類庫,功能更強大速度更快,而你爲之心動又想把這新版的類庫用到你自己的程序中,那你就必須重新編譯、重新調試!這離我們理想的積木式軟件構造方法還有一定差距,在我們的設想裏希望把一個模塊拿出來再換一個新的模塊是非常方便的事,可是現在不但要重新編譯,還要冒着很大的風險,因爲你可能要重新改變你自己的代碼。另一種重用方式很自然地就想到了是DLL的方式。Windows裏到處是DLL,它是Windows 的基礎,但DLL也有它自己的缺點。總結一下它至少有四點不足。(1)函數重名問題。DLL裏是一個一個的函數,我們通過函數名來調用函數,那如果兩個DLL裏有重名的函數怎麼辦?(2)各編譯器對C++函數的名稱修飾不兼容問題。對於C++函數,編譯器要根據函數的參數信息爲它生成修飾名,DLL庫裏存的就是這個修飾名,但是不同的編譯器產生修飾的方法不一樣,所以你在VC 裏編寫的DLLBC裏就可以用不了。不過也可以用extern "C";來強調使用標準的C函數特性,關閉修飾功能,但這樣也喪失了C++的重載多態性功能。(3)路徑問題。放在自己的目錄下面,別人的程序就找不到,放在系統目錄下,就可能有重名的問題。而真正的組件應該可以放在任何地方甚至可以不在本機,用戶根本不需考慮這個問題。(4)DLLEXE的依賴問題。我們一般都是用隱式連接的方式,就是編程的時侯指明用什麼DLL,這種方式很簡單,它在編譯時就把EXEDLL綁在一起了。如果DLL發行了一個新版本,我們很有必要重新鏈接一次,因爲DLL裏面函數的地址可能已經發生了改變。DLL的缺點就是COM的優點。首先我們要先把握住一點,COMDLL一樣都是基於二進制的代碼重用,所以它不存在類庫重用時的問題。另一個關鍵點是,COM本身也是DLL,既使是ActiveX控件.ocx它實際上也是DLL,所以說DLL在還是有重用上有很大的優勢,只不過我們通過制訂複雜的COM協議,通COM本身的機制改變了重用的方法,以一種新的方法來利用DLL,來克服DLL本身所固有的缺陷,從而實現更高一級的重用方法。COM沒有重名問題,因爲根本不是通過函數名來調用函數,而是通過虛函數表,自然也不會有函數名修飾的問題。路徑問題也不復存在,因爲是通過查註冊表來找組件的,放在什麼地方都可以,即使在別的機器上也可以。也不用考慮和EXE的依賴關係了,它們二者之間是鬆散的結合在一起,可以輕鬆的換上組件的一個新版本,而應用程序混然不覺。

二、用VC進行COM編程,必須要掌握哪些COM理論知識

  我見過很多人學COM,看完一本書後覺得對COM的原理比較瞭解了,COM也不過如此,可是就是不知道該怎麼編程序,我自己也有這種情況,我也是經歷了這樣的階段走過來的。要學COM的基本原理,我推薦的書是《COM技術內幕》。但僅看這樣的書是遠遠不夠的,我們最終的目的是要學會怎麼用COM去編程序,而不是拼命的研究COM本身的機制。所以我個人覺得對COM的基本原理不需要花大量的時間去追根問底,沒有必要,是吃力不討好的事。其實我們只需要掌握幾個關鍵概念就夠了。這裏我列出了一些我自己認爲是用VC編程所必需掌握的幾個關鍵概念。(這裏所說的均是用C++語言條件下的COM編程方式)

  (1) COM組件實際上是一個C++類,而接口都是純虛類。組件從接口派生而來。我們可以簡單的用純粹的C++的語法形式來描述COM是個什麼東西:

  class IObject
  {
  public:
    virtual Function1(...) = 0;
    virtual Function2(...) = 0;
    ....
  };
  class MyObject : public IObject
  {
  public:
    virtual Function1(...){...}
    virtual Function2(...){...}
....
  };



  看清楚了嗎?IObject就是我們常說的接口,MyObject就是所謂的COM組件。切記切記接口都是純虛類,它所包含的函數都是純虛函數,而且它沒有成員變量。而COM組件就是從這些純虛類繼承下來的派生類,它實現了這些虛函數,僅此而已。從上面也可以看出,COM組件是以 C++爲基礎的,特別重要的是虛函數和多態性的概念,COM中所有函數都是虛函數,都必須通過虛函數表VTable來調用,這一點是無比重要的,必需時刻牢記在心。爲了讓大家確切瞭解一下虛函數表是什麼樣子,從《COM+技術內幕》中COPY了下面這個示例圖:

  (2) COM組件有三個最基本的接口類,分別是IUnknownIClassFactoryIDispatch

  COM規範規定任何組件、任何接口都必須從IUnknown繼承,IUnknown包含三個函數,分別是 QueryInterfaceAddRefRelease。這三個函數是無比重要的,而且它們的排列順序也是不可改變的。QueryInterface用於查詢組件實現的其它接口,說白了也就是看看這個組件的父類中還有哪些接口類,AddRef用於增加引用計數,Release用於減少引用計數。引用計數也是COM中的一個非常重要的概念。大體上簡單的說來可以這麼理解,COM組件是個DLL,當客戶程序要用它時就要把它裝到內存裏。另一方面,一個組件也不是隻給你一個人用的,可能會有很多個程序同時都要用到它。但實際上DLL只裝載了一次,即內存中只有一個COM組件,那COM組件由誰來釋放?由客戶程序嗎?不可能,因爲如果你釋放了組件,那別人怎麼用,所以只能由COM組件自己來負責。所以出現了引用計數的概念,COM維持一個計數,記錄當前有多少人在用它,每多一次調用計數就加一,少一個客戶用它就減一,當最後一個客戶釋放它的時侯,COM知道已經沒有人用它了,它的使用已經結束了,那它就把它自己給釋放了。引用計數是COM編程裏非常容易出錯的一個地方,但所幸VC的各種各樣的類庫裏已經基本上把AddRef的調用給隱含了,在我的印象裏,我編程的時侯還從來沒有調用過AddRef,我們只需在適當的時侯調用Release。至少有兩個時侯要記住調用Release,第一個是調用了 QueryInterface以後,第二個是調用了任何得到一個接口的指針的函數以後,記住多查MSDN 以確定某個函數內部是否調用了AddRef,如果是的話那調用Release的責任就要歸你了。 IUnknown的這三個函數的實現非常規範但也非常煩瑣,容易出錯,所幸的事我們可能永遠也不需要自己來實現它們。

  IClassFactory的作用是創建COM組件。我們已經知道COM組件實際上就是一個類,那我們平常是怎麼實例化一個類對象的?是用‘new’命令!很簡單吧,COM組件也一樣如此。但是誰來new它呢?不可能是客戶程序,因爲客戶程序不可能知道組件的類名字,如果客戶知道組件的類名字那組件的可重用性就要打個大大的折扣了,事實上客戶程序只不過知道一個代表着組件的128位的數字串而已,這個等會再介紹。所以客戶無法自己創建組件,而且考慮一下,如果組件是在遠程的機器上,你還能new出一個對象嗎?所以創建組件的責任交給了一個單獨的對象,這個對象就是類廠。每個組件都必須有一個與之相關的類廠,這個類廠知道怎麼樣創建組件,當客戶請求一個組件對象的實例時,實際上這個請求交給了類廠,由類廠創建組件實例,然後把實例指針交給客戶程序。這個過程在跨進程及遠程創建組件時特別有用,因爲這時就不是一個簡單的new操作就可以的了,它必須要經過調度,而這些複雜的操作都交給類廠對象去做了。IClassFactory最重要的一個函數就是CreateInstance,顧名思議就是創建組件實例,一般情況下我們不會直接調用它,API函數都爲我們封裝好它了,只有某些特殊情況下才會由我們自己來調用它,這也是VC編寫COM組件的好處,使我們有了更多的控制機會,而VB給我們這樣的機會則是太少太少了。

  IDispatch叫做調度接口。它的作用何在呢?這個世上除了C++還有很多別的語言,比如VB VJVBScriptJavaScript等等。可以這麼說,如果這世上沒有這麼多亂七八糟的語言,那就不會有IDispatch:-) 我們知道COM組件是C++類,是靠虛函數表來調用函數的,對於VC來說毫無問題,這本來就是針對C++而設計的,以前VB不行,現在VB也可以用指針了,也可以通過VTable來調用函數了,VJ也可以,但還是有些語言不行,那就是腳本語言,典型的如 VBScriptJavaScript。不行的原因在於它們並不支持指針,連指針都不能用還怎麼用多態性啊,還怎麼調這些虛函數啊。唉,沒辦法,也不能置這些腳本語言於不顧吧,現在網頁上用的都是這些腳本語言,而分佈式應用也是COM組件的一個主要市場,它不得不被這些腳本語言所調用,既然虛函數表的方式行不通,我們只能另尋他法了。時勢造英雄,IDispatch應運而生。:-) 調度接口把每一個函數每一個屬性都編上號,客戶程序要調用這些函數屬性的時侯就把這些編號傳給IDispatch接口就行了,IDispatch再根據這些編號調用相應的函數,僅此而已。當然實際的過程遠比這複雜,僅給一個編號就能讓別人知道怎麼調用一個函數那不是天方夜潭嗎,你總得讓別人知道你要調用的函數要帶什麼參數,參數類型什麼以及返回什麼東西吧,而要以一種統一的方式來處理這些問題是件很頭疼的事。IDispatch接口的主要函數是Invoke,客戶程序都調用它,然後Invoke再調用相應的函數,如果看一看MS的類庫裏實現 Invoke的代碼就會驚歎它實現的複雜了,因爲你必須考慮各種參數類型的情況,所幸我們不需要自己來做這件事,而且可能永遠也沒這樣的機會。:-)

  (3) dispinterface接口、Dual接口以及Custom接口

  這一小節放在這裏似乎不太合適,因爲這是在ATL編程時用到的術語。我在這裏主要是想談一下自動化接口的好處及缺點,用這三個術語來解釋可能會更好一些,而且以後遲早會遇上它們,我將以一種通俗的方式來解釋它們,可能並非那麼精確,就好象用僞代碼來描述算法一樣。-:)

  所謂的自動化接口就是用IDispatch實現的接口。我們已經講解過IDispatch的作用了,它的好處就是腳本語言象VBScript JavaScript也能用COM組件了,從而基本上做到了與語言無關它的缺點主要有兩個,第一個就是速度慢效率低。這是顯而易見的,通過虛函數表一下子就可以調用函數了,而通過Invoke則等於中間轉了道手續,尤其是需要把函數參數轉換成一種規範的格式纔去調用函數,耽誤了很多時間。所以一般若非是迫不得已我們都想用VTable的方式調用函數以獲得高效率。第二個缺點就是隻能使用規定好的所謂的自動化數據類型。如果不用IDispatch我們可以想用什麼數據類型就用什麼類型,VC會自動給我們生成相應的調度代碼。而用自動化接口就不行了,因爲Invoke的實現代碼是VC事先寫好的,而它不能事先預料到我們要用到的所有類型,它只能根據一些常用的數據類型來寫它的處理代碼,而且它也要考慮不同語言之間的數據類型轉換問題。所以VC自動化接口生成的調度代碼只適用於它所規定好的那些數據類型,當然這些數據類型已經足夠豐富了,但不能滿足自定義數據結構的要求。你也可以自己寫調度代碼來處理你的自定義數據結構,但這並不是一件容易的事。考慮到IDispatch的種種缺點(它還有一個缺點,就是使用麻煩,:-) )現在一般都推薦寫雙接口組件,稱爲dual接口,實際上就是從IDispatch繼承的接口。我們知道任何接口都必須從 IUnknown繼承,IDispatch接口也不例外。那從IDispatch繼承的接口實際上就等於有兩個基類,一個是IUnknown,一個是IDispatch,所以它可以以兩種方式來調用組件,可以通過 IUnknown用虛函數表的方式調用接口方法,也可以通過IDispatch::Invoke自動化調度來調用。這就有了很大的靈活性,這個組件既可以用於C++的環境也可以用於腳本語言中,同時滿足了各方面的需要。

  相對比的,dispinterface是一種純粹的自動化接口,可以簡單的就把它看作是IDispatch接口 (雖然它實際上不是的),這種接口就只能通過自動化的方式來調用,COM組件的事件一般都用的是這種形式的接口。

  Custom接口就是從IUnknown接口派生的類,顯然它就只能用虛函數表的方式來調用接口了

  (4) COM組件有三種,進程內、本地、遠程。對於後兩者情況必須調度接口指針及函數參數。

  COM是一個DLL,它有三種運行模式。它可以是進程內的,即和調用者在同一個進程內,也可以和調用者在同一個機器上但在不同的進程內,還可以根本就和調用者在兩臺機器上。這裏有一個根本點需要牢記,就是COM組件它只是一個DLL,它自己是運行不起來的,必須有一個進程象父親般照顧它才行,即COM組件必須在一個進程內.那誰充當看護人的責任呢?先說說調度的問題。調度是個複雜的問題,以我的知識還講不清楚這個問題,我只是一般性的談談幾個最基本的概念。我們知道對於WIN32程序,每個進程都擁有4GB的虛擬地址空間,每個進程都有其各自的編址,同一個數據塊在不同的進程裏的編址很可能就是不一樣的,所以存在着進程間的地址轉換問題。這就是調度問題。對於本地和遠程進程來說,DLL 和客戶程序在不同的編址空間,所以要傳遞接口指針到客戶程序必須要經過調度。Windows 已經提供了現成的調度函數,就不需要我們自己來做這個複雜的事情了。對遠程組件來說函數的參數傳遞是另外一種調度。DCOM是以RPC爲基礎的,要在網絡間傳遞數據必須遵守標準的網上數據傳輸協議,數據傳遞前要先打包,傳遞到目的地後要解包,這個過程就是調度,這個過程很複雜,不過Windows已經把一切都給我們做好了,一般情況下我們不需要自己來編寫調度DLL

  我們剛說過一個COM組件必須在一個進程內。對於本地模式的組件一般是以EXE的形式出現,所以它本身就已經是一個進程。對於遠程DLL,我們必須找一個進程,這個進程必須包含了調度代碼以實現基本的調度。這個進程就是dllhost.exe。這是COM默認的DLL代理。實際上在分佈式應用中,我們應該用MTS來作爲DLL代理,因爲MTS有着很強大的功能,是專門的用於管理分佈式DLL組件的工具。

  調度離我們很近又似乎很遠,我們編程時很少關注到它,這也是COM的一個優點之一,既平臺無關性,無論你是遠程的、本地的還是進程內的,編程是一樣的,一切細節都由COM自己處理好了,所以我們也不用深究這個問題,只要有個概念就可以了,當然如果你對調度有自己特殊的要求就需要深入瞭解調度的整個過程了,這裏推薦一本《COM+技術內幕》,這絕對是一本講調度的好書。

  (5) COM組件的核心是IDL

  我們希望軟件是一塊塊拼裝出來的,但不可能是沒有規定的胡亂拼接,總是要遵守一定的標準,各個模塊之間如何才能親密無間的合作,必須要事先共同制訂好它們之間交互的規範,這個規範就是接口。我們知道接口實際上都是純虛類,它裏面定義好了很多的純虛函數,等着某個組件去實現它,這個接口就是兩個完全不相關的模塊能夠組合在一起的關鍵試想一下如果我們是一個應用軟件廠商,我們的軟件中需要用到某個模塊,我們沒有時間自己開發,所以我們想到市場上找一找看有沒有這樣的模塊,我們怎麼去找呢?也許我們需要的這個模塊在業界已經有了標準,已經有人制訂好了標準的接口,有很多組件工具廠商已經在自己的組件中實現了這個接口,那我們尋找的目標就是這些已經實現了接口的組件,我們不關心組件從哪來,它有什麼其它的功能,我們只關心它是否很好的實現了我們制訂好的接口。這種接口可能是業界的標準,也可能只是你和幾個廠商之間內部制訂的協議,但總之它是一個標準,是你的軟件和別人的模塊能夠組合在一起的基礎,是COM組件通信的標準。

  COM具有語言無關性,它可以用任何語言編寫,也可以在任何語言平臺上被調用。但至今爲止我們一直是以C++的環境中談COM,那它的語言無關性是怎麼體現出來的呢?或者換句話說,我們怎樣才能以語言無關的方式來定義接口呢?前面我們是直接用純虛類的方式定義的,但顯然是不行的,除了C++誰還認它呢?正是出於這種考慮,微軟決定採用IDL來定義接口。說白了,IDL實際上就是一種大家都認識的語言,用它來定義接口,不論放到哪個語言平臺上都認識它。我們可以想象一下理想的標準的組件模式,我們總是從IDL開始,先用IDL制訂好各個接口,然後把實現接口的任務分配不同的人,有的人可能善長用VC,有的人可能善長用VB,這沒關係,作爲項目負責人我不關心這些,我只關心你把最終的DLL 拿給我。這是一種多麼好的開發模式,可以用任何語言來開發,也可以用任何語言來欣賞你的開發成果。

  (6) COM組件的運行機制,即COM是怎麼跑起來的。

  這部分我們將構造一個創建COM組件的最小框架結構,然後看一看其內部處理流程是怎樣的

    IUnknown *pUnk=NULL;
    IObject *pObject=NULL;
    CoInitialize(NULL);
    CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown, (void**)&pUnk);
    pUnk->QueryInterface(IID_IOjbect, (void**)&pObject);
    pUnk->Release();
    pObject->Func();
    pObject->Release();
    CoUninitialize();

  這就是一個典型的創建COM組件的框架,不過我的興趣在CoCreateInstance身上,讓我們來看看它內部做了一些什麼事情。以下是它內部實現的一個僞代碼:

    CoCreateInstance(....)
    {
    .......
    IClassFactory *pClassFactory=NULL;
    CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pClassFactory);
    pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk);
    pClassFactory->Release();
    ........
   }

  這段話的意思就是先得到類廠對象,再通過類廠創建組件從而得到IUnknown指針。繼續深入一步,看看CoGetClassObject的內部僞碼:

   CoGetClassObject(.....)
   {
    //通過查註冊表CLSID_Object,得知組件DLL的位置、文件名
    //裝入DLL
    //使用函數GetProcAddress(...)得到DLL庫中函數DllGetClassObject的函數指針。
    //調用DllGetClassObject
   }
    DllGetClassObject是幹什麼的,它是用來獲得類廠對象的。只有先得到類廠才能去創建組件.
    下面是DllGetClassObject的僞碼:
    DllGetClassObject(...)
    {
    ......
    CFactory* pFactory= new CFactory; //類廠對象
    pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory);
    //查詢IClassFactory指針
    pFactory->Release();
    ......
    }
    CoGetClassObject的流程已經到此爲止,現在返回CoCreateInstance,看看CreateInstance的僞碼:
    CFactory::CreateInstance(.....)
    {
    ...........
    CObject *pObject = new CObject; //組件對象
    pObject->QueryInterface(IID_IUnknown, (void**)&pUnk);
    pObject->Release();
    ...........
    }

下圖是從COM+技術內幕中COPY來的一個例圖,從圖中可以清楚的看到CoCreateInstance的整個流程。

 

其實

  (7) 一個典型的自注冊的COM DLL所必有的四個函數

  DllGetClassObject:用於獲得類廠指針

  DllRegisterServer:註冊一些必要的信息到註冊表中

  DllUnregisterServer:卸載註冊信息

  DllCanUnloadNow:系統空閒時會調用這個函數,以確定是否可以卸載DLL

  DLL還有一個函數是DllMain,這個函數在COM中並不要求一定要實現它,但是在VC生成的組件中自動都包含了它,它的作用主要是得到一個全局的實例對象。

  (8) 註冊表在COM中的重要作用

  首先要知道GUID的概念,COM中所有的類、接口、類型庫都用GUID來唯一標識,GUID是一個128位的字串,根據特製算法生成的GUID可以保證是全世界唯一的。 COM組件的創建,查詢接口都是通過註冊表進行的。有了註冊表,應用程序就不需要知道組件的DLL文件名、位置,只需要根據CLSID查就可以了。當版本升級的時侯,只要改一下註冊表信息就可以神不知鬼不覺的轉到新版本的DLL

 

 

.attribute.

       (1)pointer attribute

              [unique]:

·                    Can have the value NULL.

·                  Can change during a call from NULL to non-NULL, from non-NULL to NULL, or from one non-NULL value to another.

·                  Can allocate new memory on the client. When the unique pointer changes from NULL to non-NULL, data returned from the server is written into new storage.

·                  Can use existing memory on the client without allocating new memory. When a unique pointer changes during a call from one non-NULL value to another, the pointer is assumed to point to a data object of the same type. Data returned from the server is written into existing storage specified by the value of the unique pointer before the call.

·                  Can orphan memory on the client. Memory referenced by a non-NULL unique pointer may never be freed if the unique pointer changes to NULL during a call and the client does not have another means of dereferencing the storage.

·                  Does not cause aliasing. Like storage pointed to by a reference pointer, storage pointed to by a unique pointer cannot be reached from any other name in the function.

The following restrictions apply to unique pointers:

·                  The [unique] attribute cannot be applied to binding-handle parameters ( handle_t) and context-handle parameters.

·                  The [unique] attribute cannot be applied to [out]-only top-level pointer parameters (parameters that have only the [out] directional attribute).

·                  By default, top-level pointers in parameter lists are [ref] pointers. This is true even if the interface specifies pointer_default(unique). Top-level parameters in parameter lists must be specified with the [unique] attribute to be a unique pointer.

·                  Unique pointers cannot be used to describe the size of an array or union arm because unique pointers can have the value NULL. This restriction prevents the error that results if a NULL value is used as the array size or the union-arm size.

 

[ref]:相當於C++中的const

A reference pointer has the following characteristics:

·                  Always points to valid storage; never has the value NULL. A reference pointer can always be dereferenced.

·                  Never changes during a call. A reference pointer always points to the same storage on the client before and after the call.

·                  Does not allocate new memory on the client. Data returned from the server is written into existing storage specified by the value of the reference pointer before the call.

·                  Does not cause aliasing. Storage pointed to by a reference pointer cannot be reached from any other name in the function.

A reference pointer cannot be used as the type of a pointer returned by a function.

If no attribute is specified for a top-level pointer parameter, it is treated as a reference pointer.

 

[ptr]:

The full pointer designated by the [ptr] attribute approaches the full functionality of the C-language pointer. The full pointer can have the value NULL and can change during the call from NULL to non-NULL. Storage pointed to by full pointers can be reached by other names in the application supporting aliasing and cycles. This functionality requires more overhead during a remote procedure call to identify the data referred to by the pointer, determine whether the value is NULL, and to discover if two pointers point to the same data.

Use full pointers for:

·                  Remote return values.

·                  Double pointers, when the size of an output parameter is not known.

·                  Null pointers.

 

 

 
發佈了28 篇原創文章 · 獲贊 1 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章