機動化元件

藉由 C++ 的 Polymorphism 能力,加上 dll 檔執行時期的動態連結,並配合 UUID 作為元件識別,完美建構出極具機動性的動態元件。元件的使用端可編寫出更自由、更具機動性的程式,元件的設計端只須編寫 cxxlObject 的延伸類別就能作為元件,其他的細節由本程式庫提供的元件集中管理中心(CentralizedMgr)來處理。

元件的設計端要做的事如下:

  1. 提供一個引入檔(.H 檔),內含元件所用的界面,元件以此界面實作,使用端以此界面使用元件
  2. 提供一個名為 CxxlMan_ObjectFactory 的 export 函數,CentralizedMgr 會呼叫此函數來取得所要的元件,要求的依據是 CentralizedMgr 傳過來的元件識別字串(classID)

元件的使用端要做的事如下:

  1. 先用 cxxlCM_Init() 初始化並提供元件檔(dll 檔)搜尋路徑
  2. 註冊元件,可以在程式中一個個註冊(使用 cxxlCM_ElementReg() 函數),或使用元件註冊表匯入(使用 cxxlCM_ElementImport())。這個註冊的動作主要目的,是要告知 CentralizedMgr 某個介面的 UUID 的實作是在哪個 dll。
  3. 之後就可用 cxxlCM_GetElement() 取得所要的元件
 

CxxlMan_ObjectFactory() 的格式如下:

extern "C" CXXL_DLLEXPORT
cxxlObject * cxxlCDECL CxxlMan_ObjectFactory(const wchar_t *ClassID, 
                                             void *Arg,ISpirit *spirit)
 
  • 依 ClassID 的要求傳回所要的物件,若無符合傳回 NULL。
  • Arg 提供一個參數管道,格式由程式員自定
  • spirit 使用端要求要用哪個 Spirit
 
 

cxxlCM_Init() 的原形宣告如下:

void cxxlFASTCALL 
 cxxlCM_Init(const Smart_Ptr<cxxlList<string_wrapper<wchar_t> > > &SearchPath_list); 

可藉由 cxxlList 指定多個搜尋路徑

 
 

cxxlCM_ElementReg() 的原形宣告如下:

bool cxxlFASTCALL 
  cxxlCM_ElementReg(const UNICODE_String &ClassID, const UNICODE_String &Group, 
                    const UNICODE_String &Dll);

ClassID:元件的識別字串,最好使用 UUID 以避免衝突

Group:元件自取的群組名稱做歸類,藉此可分別註冊相同介面不同實作的 dll

Dll:元件所在的元件檔名,也可包含 絕對路徑相對路徑,若是只有 檔名 或只包含 相對路徑,會到 cxxlCM_Init() 指定的搜尋路徑去尋找

若回覆失敗表示 ClassID 已註冊過了,原先的註冊不受影響

 
 
 

cxxlCM_ElementImport() 的原形宣告如下:

bool cxxlFASTCALL cxxlCM_ElementImport(std::wistream &TLC);
bool cxxlFASTCALL cxxlCM_ElementImport(
const Smart_Ptr<cxxlComposite<string_wrapper<wchar_t>,true> > &Reg);

第一種註冊表的格式採用 TLC 格式,內部會轉換後呼叫第二種註冊表的匯入函數,內容架構請見以下範例二

若失敗表示註冊表不正確(損毀)。若有相同的 ClassID 存在,原先的註冊不會受影響

 
 

cxxlCM_GetElement() 的原形宣告如下:

// 只在指定的 Group 尋找
CXXLPRESERVE_DLLEXPORT Smart_Ptr<cxxlObject> cxxlFASTCALL cxxlCM_GetElement(
const UNICODE_String &ClassID,
const UNICODE_String &Group = UNICODE_String(), //若指定為 NULL,則在所有 Group 中尋找
void *Arg = NULL,
ISpirit *spirit = Spirit_Easy);

其中的參數 Group 和 ClassI(D,即註冊的 Group 和 ClassI(D。若失敗傳回 NULL,若成功所傳回的是 ClassI(D 指定界面的 cxxlObject 實作物件,得再用 Smart_Cast 轉形成所要的界面
 
以上對幾個比較主要的功能做介紹,全部功能請看 CENTRALIZEDMGR.HPP

 

範例一

以下示範兩個簡單元件,一個加法 class Addition,一個減法 class Subtraction,這兩個元件有一個共同界面 class IUnit 宣告在 Calculate.h,我測試所用的編譯器為 vs2008,會編譯成元件檔名為 testdll.dll,完成之後即可把 Calculate.h 和 testdll.dll 釋放出去,使用端得到這兩個檔即可編寫應用程式。

 
/*******************
** Calculate.h
********************/
#ifndef CALCULATE_HPP_INCLUDED
#define CALCULATE_HPP_INCLUDED
#include <SMART_PTR.HPP>
// 算數元件介面的識別碼
#define ICalculate_ClassID L"1D30C17A1BCE4064BECDD686A00F0A33"
// 算數元件的介面
class ICalculate:virtual public CxxlMan::cxxlObject
{
public:
  ICalculate()
   :cxxlObject(CxxlMan::Spirit_Easy)
  {}
  virtual ~ICalculate(){}
  virtual int Add(int i1,int i2) = 0;
  virtual int Sub(int i1,int i2) = 0;
};
#endif // CALCULATE_HPP_INCLUDED

/**************************
** Calculate1.0.cpp
**
** ICalculate 的實作
** 編譯成 Calculate1_0.dll
**************************/
 
#include <CENTRALIZEDMGR.HPP>
#include "..\Calculate.hpp"
using namespace CxxlMan;
class CCalculate:public ICalculate
{
  virtual int Add(int i1,int i2)
  {
    return i1+i2;
  }
  virtual int Sub(int i1,int i2)
  {
    return i1-i2;
  }
public:
  CCalculate(ISpirit *spirit)
   :cxxlObject(spirit)
  {}
  virtual ~CCalculate(){}
};
 
// CentralizedMgr 會經由這個管道得到所要求的元件
extern "C" CXXL_DLLEXPORT
cxxlObject * cxxlCDECL CxxlMan_ObjectFactory(const wchar_t *ClassID,void *Arg, 
ISpirit *spirit) { if(wcscmp(ICalculate_ClassID, ClassID) == 0) return new CCalculate(spirit);
  return NULL;
}

/************************************
** test.cpp 使用端的程式碼
** 採用 cxxlCM_ElementReg() 註冊元件
************************************/
#include <iostream>
#include <wchar.h>
#include <CENTRALIZEDMGR.HPP>
#include "..\Calculate.hpp"
using namespace std;
using namespace CxxlMan;
#define StrPkg string_wrapper<wchar_t>
int main(int argc, char* argv[])
{
setlocale(LC_CTYPE, "");            // 指定系統現正使用中的地區語言做轉換
// 以主程式(exe 檔)所在路徑為搜尋路徑,因元件檔(dll 檔)置於同一資料夾中 UNICODE_String BasePath(argv[0]); *(wchar_t*)wcsrchr((const wchar_t*)BasePath,L'\\') = L'\0'; Smart_Ptr<cxxlList<StrPkg > > BasePath_List(new cxxlList<StrPkg >); BasePath_List->Push_Back(BasePath); cxxlCM_Init(BasePath_List);
  // 註冊兩個版本的元件,不過這個範例並沒有實作 1.1 版
  cxxlCM_ElementReg(ICalculate_ClassID, L"Calculate v1.0", L"Calculate1_0.dll");
  cxxlCM_ElementReg(ICalculate_ClassID, L"Calculate v1.1", L"Calculate1_1.dll");
  // 準備兩個測試的數值
  int i1 = 5;
  int i2 = 7;
  // 取得 1.0 版的元件
  Smart_Ptr<ICalculate> Cal_Ptr = Smart_Cast<ICalculate>( 
cxxlCM_GetElement(ICalculate_ClassID, L"Calculate v1.0") );
  cout << i1 << " + " << i2 << " = " << Cal_Ptr->Add(i1,i2) << endl;
  cout << i1 << " - " << i2 << " = " << Cal_Ptr->Sub(i1,i2) << endl;
  system("pause");
  return 0;
}
 
在 Win32 平台下,CentralizedMgr 內部採用 LoadLibraryW() 載入 dll 檔,但 LoadLibraryW() 這個函數算是半調子的 Unicode 版本,內部還是再轉為 ANSI,因此範例中才多加了 setlocale(LC_CTYPE, ""); 幫它做正確的轉碼。
 
 

範例二

接下來示範使用元件註冊表來代替一個個註冊元件的方式,元件註冊表的格式採用 TLC 格式,最上層節點代表 Group,其下的各節點代表元件的註冊項目,各項目的節點名稱即為 ClassID,內容為 "只包含元件檔名",或是 "包含相對路徑的元件檔名",或是 "包含絕對路徑的元件檔名",若是 "只包含元件檔名" 或 "包含相對路徑的元件檔名",則在索取元件時會到 cxxlCM_Init() 指定的搜尋路徑中去尋找。

一般的做法應會把元件註冊表做成檔案,以下的範例為了簡明,就直接在程式建立,否則應用 wifstream 來代替 wistringstream。

/****************************************
** test.cpp 使用端的程式碼
** 採用 cxxlCM_ElementImport() 註冊元件
****************************************/
#include <iostream>
#include <wchar.h>
#include <CENTRALIZEDMGR.HPP>
#include "..\Calculate.hpp"
using namespace std;
using namespace CxxlMan;
// 產生 1.0 版的註冊表
wstring CreateRegTbl_1_0()
{
  wstring RegTbl(
    L"[Calculate v1.0]"
    L"{"
    L"  [1D30C17A1BCE4064BECDD686A00F0A33] = \"Calculate1_0.dll\""
    L"  [虛擬一個介面] = \"Calculate1.0.dll\""
    L"  [虛擬一個介面和元件] = \"xxx.dll\""
    L"}");

   return RegTbl;
}
// 產生 1.1 版的註冊表
wstring CreateRegTbl_1_1()
{
  wstring RegTbl(
    L"[Calculate v1.1]"
    L"{"
    L"  [1D30C17A1BCE4064BECDD686A00F0A33] = \"Calculate1_1.dll\""
    L"  [虛擬一個介面] = \"Calculate1.1.dll\""
    L"  [虛擬一個介面和元件] = \"xxx.dll\""
    L"}");

  return RegTbl;
}
#define StrPkg string_wrapper<wchar_t>
int main(int argc, char* argv[])
{
setlocale(LC_CTYPE, "");            // 指定系統現正使用中的地區語言做轉換
// 以主程式(exe 檔)所在路徑為搜尋路徑,因元件檔(dll 檔)置於同一資料夾中 UNICODE_String BasePath(argv[0]); *(wchar_t*)wcsrchr((const wchar_t*)BasePath,L'\\') = L'\0'; Smart_Ptr<cxxlList<StrPkg > > BasePath_List(new cxxlList<StrPkg >); BasePath_List->Push_Back(BasePath); cxxlCM_Init(BasePath_List);
  // 註冊兩個版本的元件,不過這個範例並沒有實作 1.1 版
  wistringstream Wins10( CreateRegTbl_1_0() );
  cxxlCM_ElementImport(Wins10);
  wistringstream Wins11( CreateRegTbl_1_1() );
  cxxlCM_ElementImport(Wins11);
  // 準備兩個測試的數值
  int i1 = 5;
  int i2 = 7;
  // 取得 1.0 版的元件
  Smart_Ptr<ICalculate> Cal_Ptr = Smart_Cast<ICalculate>( 
cxxlCM_GetElement(ICalculate_ClassID, L"Calculate v1.0") );
  cout << i1 << " + " << i2 << " = " << Cal_Ptr->Add(i1,i2) << endl;
  cout << i1 << " - " << i2 << " = " << Cal_Ptr->Sub(i1,i2) << endl;
  system("pause");
  return 0;
}
 
 範例可到本文結尾處下載
 

物件的生命期與元件檔的卸載

當用 cxxlCM_GetElement() 成功得到一個物件時,那麼產生這個物件的元件檔(dll 檔)的計數器會加 1,而這個物件結束時,元件檔的計數器會減 1,當計數器歸 0 時,這個元件檔便會被卸載,但當元件檔卸載後是不是這個元件檔所提供物件都已結束了?元件檔提供的所有物件全都是由 cxxlCM_GetElement() 產生的這是沒有問題的,但若非如此就問題就嚴重了。

一般來說最有可能發生這問題的情況是,cxxlCM_GetElement() 所產生的物件額外再提供物件給使用端,但這物件並沒有經 cxxlCM_GetElement() 來產生,而這物件的程式碼又存在這個元件檔中。面對這些問題的解決辦法有兩種:

  1. 不把元件檔卸載是最可靠辦法,可以由使用端或元件檔內用 cxxlCM_GetElement() 產生永不結束的物件就可以了,缺點是不卸載較佔用資源。
  2. 對於提供給使用端的物件都須用 cxxlCM_GetElement() 產生

若採用第 1 種方法,第 2 的要求就非必要了,若不採用第 1 種方法,則第 2 的要求一定要遵守。

 

CxxlMan_ObjectFactory() 不可以直接傳回 cxxlCM_GetElement() 的物件

cxxlCM_GetElement() 取得的物件會被 CentralizedMgr 再處理,因此 CxxlMan_ObjectFactory() 若再直接傳回 cxxlCM_GetElement() 取得的物件,會再被 CentralizedMgr 處理一次,這會造成很大的問題。標準的做法是把 cxxlCM_GetElement() 取得的物件置於 CxxlMan_ObjectFactory() 要傳回的物件中。

 
 
 
引入檔

CENTRALIZEDMGR.HPP

程式庫

cxxlpreserve

 
 
 
 
 
ċ
本文範例下載.zip
(9k)
Cxxlman Cxxlman,
2012年4月6日 下午5:58
Comments