理解代碼的二進制級別重用

理解代碼的二進制級別重用

 

在軟件開發中,經常提到源代碼重用,Dll重用等概念,而代碼的二進制級別重用則相對晦澀。本文將從軟件發佈的角度一步一步講解二進制級別重用的內涵,希望對大家有幫助。需要說明的是,在行文過程中,默認使用了C++作爲程序開發語言。

一、源代碼重用

源代碼重用是指軟件廠商提供包含頭文件和源文件的完整代碼供客戶使用。比如,一軟件廠商發佈一個類庫源代碼CMath四則運算)此時,在開發客戶應用程序時,需要引用CMath的頭文件和源文件,編譯得到的應用程序中包含CMath對應的二進制代碼。CMath類所產生的代碼佔4MB的空間,那麼當三個客戶應用程序都使用CMath庫時,每個可執行文件都包含4MB的類庫代碼。其示意圖如圖1。源代碼重用的問題主要有兩個:一是增加客戶端程序大小;二是更新發布時需要客戶應用程序重新編譯

wKiom1l3UIeAU7tkAABzWJqlK2g475.png 

1 源代碼重用

二、DLL重用

解決上述問題,一種衆所知之技術是將CMath類做成動態鏈接庫。但是,一旦確定要以DLL形式發佈一個C++類,將面臨C++的基本弱點之一:缺少二進制一級的標準。比如名字改編導致各編譯器不兼容,雖然模塊定義文件可以減輕這個問題,但還有其他與最終產生的代碼相關的更多問題。除了最簡單的語言結構外,所有的編譯器廠商都會選擇自己專有的方法來實現語言的其他特性,從而使其他的編譯器無法使用它所產生的函數。異常就是這些語言特性的一個典型例子。Microsoft編譯器編譯產生的函數,它所拋出的一場不能被Watcom編譯器編譯生成的客戶程序捕捉到。C++通過private和public關鍵字確實支持語法上的封裝性,但是C++草案標準病沒有定義二進制層次上的封裝性。C++編譯模型要求編譯器能夠訪問與對象內存佈局有關的所有信息,這樣才能夠構造實例,或調用類的非虛成員函數。這些信息包括私有成員和公共成員的大小和順序。

如果上述闡述過於抽象,那麼下面將引用《COM本質論》中的例子進行解釋。假設一軟件廠商以DLL方式提供FastString類供客戶使用,具體代碼如下:

class __declspec(dllexport) FastString{
char *m_psz;
public:
FastString(const char *psz);
~FastString(void);
int Length(void) const; // 返回字符數目
int Find(const char *psz) const; // 返回偏移量
};


此時,客戶應用程序與廠商提供的DLL之間的關係如圖2所示。

wKioL1l3UJ_g6SHmAABOhtxiwPM105.png-wh_50 

2 DLL重用

DLL重用雖然解決了增加客戶應用程序體積的問題,但是並沒有完全解決需要客戶應用程序需要重新編譯的問題。現在假設廠商對FastString進行了升級,添加了一個私有成員變量,提供給客戶2.0版本的DLL、DLL引入庫和頭文件。其中頭文件爲:

class __declspec(dllexport) FastString{
const int m_cch; // 字符數
char *m_psz;
public:
FastString(const char *psz);
~FastString(void);
int Length(void) const; // 返回字符數目
int Find(const char *psz) const; // 返回偏移量
};


此時,假設客戶應用程序沒有重新編譯,而FastString.dll升級成了2.0版本。那麼客戶應用程序還是認爲FastString對象佔用4字節空間,而實際上新的FastString對象佔用了8字節空間。但客戶應用程序調用了Find函數去訪問成員變量m_psz時,由於客戶應用程序並沒有分配相應的空間(Find函數認爲前4個字節是成員變量m_cch),從而導致函數返回錯誤的結果或者應用程序崩潰,也就是說DLL重用沒有實現二進制級別的重用。

wKiom1l3ULOiI4DWAABSx0mRxrs865.png 

3 DLL重用存在的問題

三、二進制級別重用

在《COM本質論》中闡述了兩種二級制級別的重用方案:句柄類和抽象類。上述兩種方法都能夠確保DLL內部實現的變化不會對客戶應用程序產生影響,開發者無需重新編譯程序。因爲客戶應用程序編譯運行所需的對象的內存佈局信息沒有改變,實現了二進制層次上的封裝。

1、句柄類

還是上述的FastString類爲例,提供給客戶的不再是FastString實體類,而是包含一個實體類指針的句柄類FastStringItf頭文件,其對應的函數實現就是調用實體類FastString的同名函數,就不給出具體代碼了。FastStringItf.h如下:

// faststringitf.h
class __declspec(dllexport) FastStringItf {
class FastString;
FastString *m_pThis;
public:
FastStringItf(const char *psz);
~FastStringItf(void);
int Length(void) const;
int Find(const char *psz) const;
};


使用句柄類之後,DLL重用一節中FastString的1.0版和2.0版提供給客戶的都是同樣的句柄類,1.0版應用程序所認知到的FastStringItf類與2.0版本的FastString.dll是一致的(佔用4字節,相同的函數佈局),因此客戶應用程序無需重新編譯。

2、抽象類

繼續以FastString類爲例,定義一個抽象類IFastString:

// ifaststring.h
class IFastString {
public:
virtual int Length(void) const = 0;
virtual int Find(const char *psz) const = 0;
}


FastString 1.0版本的代碼爲:

class FastString:IFastString {
char *m_psz;
public:
FastString(const char *psz);
~FastString(void);
int Length(void) const; // 返回字符數目
int Find(const char *psz) const; // 返回偏移量
};


FastString 2.0版本的代碼爲:

class FastString:IFastString {
const int m_cch;
char *m_psz;
public:
FastString(const char *psz);
~FastString(void);
int Length(void) const; // 返回字符數目
int Find(const char *psz) const; // 返回偏移量
};


不管是1.0版本還是2.0版本,提供給客戶的只是抽象類的定義IFastString,該抽象類的內存空間佈局沒有隨着DLL的升級而改變,因此客戶應用程序也無需重新編譯。

PS:對於一些大型的軟件系統,都會考慮到二進制級別的重用。比如QT就廣泛使用了句柄類模式的二進制重用技術,其對外提供服務的類都是句柄類,真正實現具體功能的是對應的private類(也就是上文所說的實體類),比如QWebEnginePage對應的private類是QWebEnginePagePrivate。對二進制重用有一定的瞭解,就不難理解QT源代碼中Q_D、Q_Q等宏的意圖和實現方法了。

 

參考

COM本質論

C++編程思想》

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章