線上訂房服務-台灣趴趴狗聯合訂房中心
發文 回覆 瀏覽次數:1448
推到 Plurk!
推到 Facebook!

關於用 C++Builder 進行 MIDAS 應用開發的討論

 
conundrum
尊榮會員


發表:893
回覆:1272
積分:643
註冊:2004-01-06

發送簡訊給我
#1 引用回覆 回覆 發表時間:2004-06-06 14:37:32 IP:61.221.xxx.xxx 未訂閱
  http://dev.csdn.net/develop/article/15/15114.shtm    標題   關於用 C  Builder 進行 MIDAS 應用開發的討論     Raptor [原作]  
關鍵字   BCB MIDAS m_DataModule _Module 
出處    
 
     914事件發生的那天,Ben_Ladan(蘭企鵝)兄在 CSDN 的 BCB 版發了一個貼,問起一個關於用 BCB 進行 MIDAS 開發的問題。剛好這個問題是我會的,因為一年多前(準確的說是2001年9月4日) luhongjun(過江項羽)兄曾在 BCB 版發過一個關於 MIDAS 開發的貼子,其中就有類似的問題,當年解決項羽兄的兩個問題也是 BCB 版的高人:holyfire(^@L@^)和ALNG(?)。很高興這次的討論又加入了幾位新的高人:kingcaiyao(AKing)、PPower()等,將這個問題的研究更加深化了,相信看過此貼的人都獲益非淺。我把這些內容整理了一下,並作了一些進一步的分析。        為方便討論,先寫一個例副程式,與 Ben_Ladan 兄的程式及 luhongjun 兄的不同,但原理是一樣的: 
    服務端程式如圖所示:            這是一個普通的 RemoteDataModule 式的 MIDAS 中間伺服器。使用一個 ADOConnection 、一個 ADODataSet 、一個 DataSetProvider 。其中 adoTest(ADODataSet) 的 CommandText 屬性沒有設置。在 IrdmTest 介面中增加一個方法: SetQuery ,帶有一個 BSTR 類型的參數:aTableName ,如下圖:            刷新後生成 SetQuery 方法的實現,完成其實現如下(其中標記為紅色的 m_DataModule 是後面要討論的問題之一):    STDMETHODIMP TrdmTestImpl::SetQuery(BSTR aTableName)
{
    try
    {
        m_DataModule->adoTest->CommandText = AnsiString( "select * from " )   aTableName;
    }
    catch( Exception &e )
    {
        return Error(e.Message.c_str(), IID_IrdmTest);
    }
    return S_OK;
}       將服務端程式編譯註冊。然後寫一個用戶端程式,如下圖所示:           生成兩個按鈕的事件回應並實現,代碼如下(下面標記為紅色的部分是另一個要討論的問題):     //---------------------------------------------------------------------------    //  SetQuery
void __fastcall TForm2::Button1Click(TObject *Sender)
{
    Conn->Open( );
    IrdmTestDisp v;
    v.Bind( ( IDispatch * )Conn->AppServer );
    try {
        v->SetQuery( WideString( Edit1->Text ) );
        cdsTest->Open( );
    }
    catch ( ... ) {
    }
    v.Unbind( );
    //  這裏就可以運算元據了
}
//---------------------------------------------------------------------------
//  CloseAll
void __fastcall TForm2::Button3Click(TObject *Sender)
{
    cdsTest->Close( );
    Conn->Close( );
}
//---------------------------------------------------------------------------       上面這個程式是可以正常運行的。現在可以開始討論前面說的兩個問題了:    為什麼在服務端必須使用 m_DataModule 來訪問遠端資料模組,而不能用 rdmTest 這個全局指標變數? 
為什麼在用戶端必須使用 Bind( ( IDispatch * )Conn->AppServer ) 來綁定服務端的調度介面(dispinterface),而不能用 BindDefault( ) 來綁定? 
   首先來看第一個問題:熟悉 Delphi 的人一定會很奇怪 SetQuery 為什麼是 TrdmTestImpl 類的成員,而不是 rdmTest 類的成員,而且在 Delphi 中,根本就沒有 TrdmTestImpl 類。這是因為 Delphi 和 C   Builder 在 COM 方面的實現技術不同所致,在 Delphi 中是用 Borland 自己開發的 Framework : DAX 的,而在 C   Builder 中是用 ATL 來實現。看看 DataBrk.pas 單元中 Delphi 的 RemoteDataModule 和 C   Builder 的 RemoteDataMoudle 就知道了,下面是一個類圖:           圖中的 TCRemoteDataModule 就是為 C   Builder 準備的 RemoteDataModule 類,它與 Delphi 所用的 TRemoteDataModule 類最大的不同就是:它沒有實現 IAppServer 介面,而 TRemoteDataModule 則有。圖上 TrdmTest 即為前面例副程式中的 RemoteDataModule ,而 IDrdmTest/TDrdmTest 是為了對比加上的 Delphi 相應的介面和類。可見, Delphi 只要這幾個介面/類即可實現 MIDAS 的核心功能,但 C   Builder 則不行,它還沒實現 IAppServer 介面。
   那麼 C   Builder 是如何實現的呢?看下面這個類圖,它是根據 rdmTestImpl.h 檔的內容畫的(注意:下圖中的Stereotype被用來說明模組板參數):           當然,在 rdmTestImpl.h 檔中用了宏來實現 TrdmTestImpl 類的派生,上圖是將其展開後並向上了幾個層次。很明顯,這是一個多重派生,TrdmTestImpl 類從 ATL 的範本類 CComObjectRootEx 和 CComCoClass 以及 IAppServer 介面的實現類 IAppServerImpl 中派生出來的,而在前一圖中: TrdmTest 是從 VCL 類 TCRemoteDataModule 中派生。我們知道 VCL 類是不支持多重派生的,所以在 C   Builder 中不得不作另外的處理。
   首先,可以看到 IrdmTest 介面是從 IAppServer 派生來的,所以 SetQuery 是 IrdmTest 的一個方法,當然也就是 TrdmTestImpl 的成員,而不是 TrdmTest 的成員。圖中 IDispatchImpl, IAppServerImpl, TrdmTestImpl 三個類分別是 IDispatch, IAppServer, IrdmTest 三個介面的實現,其中 IDispatchImpl 是從 IrdmTest 介面派生的,這是通過範本來實現的。
   因為在 MIDAS 中,用戶端是通過創建一個 Remote COM 物件來調用服務端的 IAppServer 介面的,即當用戶端連接到服務端來時,服務端將啟動一個 IAppServerImpl 的實例。為了連接 RemoteDataModule , IAppServerImpl 類通過範本實現了一個 TrdmTest 類的實例: m_DataModule ,因為每個用戶端連接對應了一個 IAppServerImpl 的實例,也就是說,每個用戶端有一個各自獨立的 RemoteDataModule 實例。下面這段代碼來自 atlvcl.h 文件,也就是bufanxiong(bufanxiong)兄貼出的那段代碼,它說明了 IAppServerImpl 是如何通過 m_DataModule 連接 RemoteDataModule 的。(注意:其中的 DM 類其實是通過範本參數傳入的類型,在本例中即是 TrdmTest )      DM* m_DataModule;            // The Core. 
  // Note: This data module _must_ descend from TCRemoteDataModule.      IAppServerImpl()
  {
   m_DataModule = new DM(NULL);
  }      ~IAppServerImpl() 
  {
    m_DataModule->Free();
  }       而那個全局的 rdmTest 物件指標指向的肯定不會是與當前 IAppServerImpl 實例關聯的 RemoteDataModule 實例,所以在問題一中,必須使用 m_DataModule 而不能用 rdmTest ,否則調用 SetQuery 設置 CommandText 屬性的 ADODataSet 肯定跟 ClientDataSet Open 時打開的那個 ADODataSet 不是同一個,當然會出錯。基本上 PPower() 兄的理解是比較正確的,不過要注意的是 IAppServerImpl 不是來自於 ATL 的,它是 BCB 的 MIDAS 的一部分,至於用 ATL 實現的 MIDAS 是否會比用 DAX 實現的 MIDAS 要快就很難說了。但可以肯定的是用 DAX 做肯定比用 ATL 簡單。        再來看第二個問題,為什麼不能用 BindDefault 。
   首先看看 Server_TLB.h 檔中 IrdmTestDisp 類中 BindDefault 是如何實現的:    // *********************************************************************//
// DispIntf:  IrdmTest
// Flags:     (4416) Dual OleAutomation Dispatchable
// GUID:      {1BB59F63-93D0-4FC2-9D5C-5DAE096C38D2}
// *********************************************************************//
template
class IrdmTestDispT : public TAutoDriver
{
public:
  ...      HRESULT BindDefault()
  {
    return OLECHECK(Bind(CLSID_rdmTest));
  }    typedef IrdmTestDispT IrdmTestDisp;       它調用了 TAutoDriver 範本類中 Bind 函數的一個重載版本,見 utilcls.h    // Bind via GUID
//
template  HRESULT
TAutoDriver::Bind(const GUID& clsid)
{
  LPUNKNOWN punk = 0;
  HRESULT hr = CoClassCreator::CoCreateInstance(clsid, IID_IUnknown, (LPVOID*)&punk);
  if (SUCCEEDED(hr))
  {
    // We should have a valid interface pointer
    //
    _ASSERTE(punk /* Must have valid IUnknown pointer */);        // Run Object - just in case
    //
    hr = ::OleRun(punk);        // Bind to running IUnknown
    //
    if (SUCCEEDED(hr))
      hr = Bind(punk);        // Release IUnknown
    //
    punk->Release();
  }
  return hr;
}       請注意上面標記為紅色部分的代碼, Bind 在這裏調用了 CoCreateInstance 來創建了一個新的服務端實例,即服務端會創建一個新的 IAppServerImpl 實例,它與用戶端程式通過 DCOMConnection 連接來創建的那個 IAppServerImpl 實例是相互獨立的。這就意味著:如果用戶端通過 BindDefault 連接到服務端,然後調用 SetQuery 進行操作,當用戶端調用 IrdmTestDisp 類的 Unbind 函數時,此 IAppServerImpl 實例將被釋放,也就是說剛才用 SetQuery 所作的操作被全部作廢,對那個與 DCOMConnection 連接的 IAppServerImpl 實例沒有任何影響,因為它們根本不是同一個,當然會出錯了。
   再看看如果改用 Bind( ( IDispatch * )Conn->AppServer ) 又是如何呢?
   這時將調用 TAutoDriver 範本類中 Bind 函數的另一個重載版本,見 utilcls.h    // Bind via IUnknown
//
template  HRESULT
TAutoDriver::Bind(LPUNKNOWN punk)
{
  _ASSERTE(punk /* Must bind to non-NULL interface pointer */);
  HRESULT hr = E_POINTER;
  if (punk)
  {
    DISPINTF *disp;
    hr = punk->QueryInterface(__uuidof(DISPINTF), (LPVOID*)&disp);
    if (SUCCEEDED(hr))
      Bind(disp, false /* Don't AddRef */);
  }
  return hr;
}       這時就不會新建實例,而是通過 QueryInterface 來取得相應的實例指標了。至於為什麼要把 Conn->AppServer 轉為 IDispatch * 是因為: AppServer 是 Variant 類型(為了方便進行 Late binding 方式調用),而 Variant 類型是 IDispatch * 的相容類型, IDispatch 又是從 IUnknown 派生的,所以需要先轉換一下,否則會出現類型不相容的編譯錯誤。       最後順便說一下 Ben_Ladan(蘭企鵝)兄提到的另一個問題,在 Server.cpp 中:    TComModule _ProjectModule( 0/*InitATLServer*/);
TComModule &_Module = _ProjectModule;    // The ATL Object map holds an array of _ATL_OBJMAP_ENTRY structures that
// described the objects of your OLE server. The MAP is handed to your
// project's CComModule-derived _Module object via the Init method.
//
BEGIN_OBJECT_MAP(ObjectMap)
  OBJECT_ENTRY(CLSID_rdmTest, TrdmTestImpl)
END_OBJECT_MAP()       其中的 _Module 是做什麼用的?
   PPower() 兄的解釋是不對的。因為下面那段關於 MAP 的代碼並非用於 VCL 類和 C   類之間 MAP 的,但它跟 _Module 也不是沒有關係。
   首先,見 atlcom.h 中這三個宏的定義(因為代碼太長,作了斷行處理):    #define BEGIN_OBJECT_MAP(x) static _ATL_OBJMAP_ENTRY x[] = {
#define END_OBJECT_MAP()   {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}};
#define OBJECT_ENTRY(clsid, class) {&clsid, class::UpdateRegistry, \ 
    class::_ClassFactoryCreatorClass::CreateInstance, \ 
    class::_CreatorClass::CreateInstance, NULL, 0, class::GetObjectDescription, \ 
    class::GetCategoryMap, class::ObjectMain },       可見這段 MAP 只是用於建立一個 _ATL_OBJMAP_ENTRY 類的陣列,下面是 atlbase.h 中關於這個類的定義:    struct _ATL_OBJMAP_ENTRY
{
  const CLSID* pclsid;
  HRESULT (WINAPI *pfnUpdateRegistry)(BOOL bRegister);
  _ATL_CREATORFUNC* pfnGetClassObject;
  _ATL_CREATORFUNC* pfnCreateInstance;
  IUnknown* pCF;
  DWORD dwRegister;
  _ATL_DESCRIPTIONFUNC* pfnGetObjectDescription;
  _ATL_CATMAPFUNC* pfnGetCategoryMap;
  HRESULT WINAPI RevokeClassObject()
  {
    return CoRevokeClassObject(dwRegister);
  }
  HRESULT WINAPI RegisterClassObject(DWORD dwClsContext, DWORD dwFlags)
  {
    IUnknown* p = NULL;
    if (pfnGetClassObject == NULL)
      return S_OK;
    HRESULT hRes = pfnGetClassObject(pfnCreateInstance, IID_IUnknown, (LPVOID*) &p);
    if (SUCCEEDED(hRes))
      hRes = CoRegisterClassObject(*pclsid, p, dwClsContext, dwFlags, &dwRegister);
    if (p != NULL)
      p->Release();
    return hRes;
  }
// Added in ATL 3.0
  void (WINAPI *pfnObjectMain)(bool bStarting);
};       這下很清楚了, _ATL_OBJMAP_ENTRY 類是用於 COM 物件初始化的,它提供了物件的註冊/反註冊/CreateInstance/GetClassObject(用於取得 ClassFactory)等功能。當一個程式中包含了多個 COM 類時就需要用這個資料來維護各個類的初始化實現。
   那麼這些初始化實現與 _Module 有什麼關係?
   其實 _Module 很像 DAX 中的 ComServer 。下面的代碼來自 atlmod.h 中, _Module 和 ObjectMap(就是前面用巨集定義的陣列)就是用在這裏了。    // _Module is assumed to be a reference to a TComModule
// User may define _Module to be a ref. to an instance of a class derived
// from TComModule.
//
typedef TATLModule TComModule;
extern TComModule &_Module;    ...    // To be defined in the Project's source
//
extern _ATL_OBJMAP_ENTRY ObjectMap[];       仔細研究 atlmod.h 中關於 TATLModule 類型的實現就可以發現,它的構造函數調用了成員函數 InitATLServer ,這就是為什麼在 Server.cpp 中會有:TComModule _ProjectModule( 0/*InitATLServer*/);,而在 InitATLServer 中,_Module 調用了 CComModule 的成員 Init 進行初始化,其中用到的一個參數就是 ObjectMap 。
   至此 _Module 的用途已經完全清楚了:它是 COM 應用程式的初始化類的實現物件,而它是通過 ObjectMap 陣列進行對 COM 應用程式中不同的 COM 類物件進行初始化的。
   下面這段代碼來自用 BCB 寫的 DLL 方式的 COM 。四個初始化函數據都被映射為 _Module 的相應函數調用了。     // Entry point of your Server invoked to inquire whether the DLL is no
// longer in use and should be unloaded.
//
STDAPI __export DllCanUnloadNow(void)
{
    return (_Module.GetLockCount()==0) ? S_OK : S_FALSE;
}    // Entry point of your Server allowing OLE to retrieve a class object from
// your Server
//
STDAPI __export DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
    return _Module.GetClassObject(rclsid, riid, ppv);
}    // Entry point of your Server invoked to instruct the server to create
// registry entries for all classes supported by the module
//
STDAPI __export DllRegisterServer(void)
{
    return _Module.RegisterServer(TRUE);
}    // Entry point of your Server invoked to instruct the server to remove
// all registry entries created through DllRegisterServer.
//
STDAPI __export DllUnregisterServer(void)
{
    return _Module.UnregisterServer();
}       這種技術性的討論是非常有益的,為了寫本文,我在這一周裏讀了 BCB 中的很多源碼,覺得大有收穫,希望本文對讀者也有一定的幫助。         
系統時間:2024-05-04 8:21:30
聯絡我們 | Delphi K.Top討論版
本站聲明
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。
2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。
3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇!