DLL的向后兼容性问题
DLL的向后兼容性問題
本文將要介紹DLL的向后兼容性問題,也就是著名的“DLL
Hell”問題。首先我會(huì)列出自己的研究結(jié)果,其中包括其它一些研究者的成果。在本文的最后,我還將給出“DLL Hell”問題的一個(gè)解決方案。
介紹
我曾經(jīng)接受過一個(gè)任務(wù),去解決一個(gè)DLL版本更新的問題————某個(gè)公司給用戶提供了一套SDK,這個(gè)SDK是由一系列DLL組成的;DLL中導(dǎo)出了很
多類,用戶使用這些類(直接使用或派生新的子類)來繼續(xù)他們的C++程序開發(fā)。用戶在使用這些DLL時(shí)沒有得到很詳細(xì)的使用說明(比如使用這些DLL中導(dǎo)
出的類有什么限制等)。當(dāng)這些DLL更新為新的版本之后,他們發(fā)現(xiàn)他們開發(fā)的基于這些DLL的應(yīng)用程序會(huì)經(jīng)常崩潰(他們的應(yīng)用程序從SDK的導(dǎo)出類派生了
新的子類)。為了解決這個(gè)問題,用戶必須重新編譯他們的應(yīng)用程序,重新連接新版本的SDK DLL。
我將對(duì)這個(gè)問題給出我的研究結(jié)果,同時(shí)還有我從其它地方搜集過來的相關(guān)信息。最后,我將來解決這個(gè)“DLL Hell”問題。
研究結(jié)果
就我個(gè)人的理解,這個(gè)問題是由SDK
DLL中導(dǎo)出的基類改動(dòng)之后引起的。我查看了一些文章后發(fā)現(xiàn),DLL的向后兼容性問題其實(shí)早有人提出。但作為一個(gè)實(shí)在的研究者,我決定自己做一些試驗(yàn)。結(jié)
果,我發(fā)現(xiàn)如下的問題:
1. 在DLL的導(dǎo)出類中增加一個(gè)新的虛函數(shù)將導(dǎo)致如下問題:
(1)如果這個(gè)類以前就有一個(gè)虛函數(shù)B,此時(shí)在它之前增加一個(gè)新的虛函數(shù)A。這樣,我們改變了類的虛函數(shù)表。于是,表中的第一個(gè)函數(shù)指向了函數(shù)A(而不
是原來的B)。此時(shí),客戶程序(假設(shè)沒有在拿到新版本的DLL之后重新編譯、連接)調(diào)用函數(shù)B就會(huì)產(chǎn)生異常。因?yàn)榇藭r(shí)調(diào)用函數(shù)B實(shí)際上轉(zhuǎn)向了調(diào)用函數(shù)A,
而如果函數(shù)A和函數(shù)B的參數(shù)類型、返回值類型迥異的話問題就出來了!
?。?)如果這個(gè)類原本沒有虛函數(shù)(它的父類也沒有虛函數(shù)),那么
給這個(gè)類增加一個(gè)新的虛函數(shù)(或者在它的父類增加一個(gè)虛函數(shù))將導(dǎo)致新增加一個(gè)類成員,這個(gè)成員是一個(gè)指針類型的,指向虛函數(shù)表。于是,這個(gè)類的尺寸將會(huì)
被改變(因?yàn)樵黾恿艘粋€(gè)成員變量)。這種情況下,客戶程序如果創(chuàng)建了這個(gè)類的實(shí)例,并且需要直接或間接修改類成員的值的時(shí)候就會(huì)有問題了。因?yàn)樘摵瘮?shù)表的
指針是作為類的第一個(gè)成員加入的,也就是說,原本這個(gè)類定義的成員因?yàn)樘摵瘮?shù)表指針的加入而都產(chǎn)生了地址的偏移。客戶程序?qū)υ蓡T的操作自然就出現(xiàn)異常
了。
?。?)如果這個(gè)類原本就有虛函數(shù)(或者只要它的父類有虛函數(shù)),而且這個(gè)類被導(dǎo)出了,被客戶程序當(dāng)作父類來用。那么,我們不
要給這個(gè)類增加虛函數(shù)!不僅在類聲明的開頭不能加,即使在末尾處也不能加。因?yàn)榧尤胩摵瘮?shù)會(huì)導(dǎo)致虛函數(shù)表內(nèi)的函數(shù)映射產(chǎn)生偏移;即使你將虛函數(shù)加在類聲明
的末尾,這個(gè)類的派生類的虛函數(shù)表也會(huì)因此產(chǎn)生偏移。
2. 在DLL的導(dǎo)出類中增加一個(gè)新的成員變量將導(dǎo)致如下問題:
?。?)給一個(gè)類增加一個(gè)成員變量將導(dǎo)致類尺寸的改變(給原本有虛函數(shù)表的類增加一個(gè)虛函數(shù)將不會(huì)改變類的尺寸)。假設(shè)這個(gè)成員增加在類聲明的最后。如
果客戶程序?yàn)閯?chuàng)建這個(gè)類的實(shí)例少分配了內(nèi)存,那么可能在訪問這個(gè)成員時(shí)導(dǎo)致內(nèi)存越界。
?。?)如果在原有的類成員中間增加一個(gè)新的成員,情況會(huì)更糟糕。因?yàn)檫@樣會(huì)導(dǎo)致原有類成員的地址產(chǎn)生偏移。客戶程序操作的是一個(gè)錯(cuò)誤的地址表,對(duì)于新
成員后面的成員尤其是這樣(它們都因?yàn)樾鲁蓡T的加入而導(dǎo)致了自己在類中的偏移的變化)。
?。ㄗⅲ荷鲜龅目蛻舫绦蚓褪侵甘褂肧DK DLL的應(yīng)用程序。)
除了上面這些原因外,還有其它操作會(huì)導(dǎo)致DLL的向后兼容性問題。下面列出了解決(大部分)這些問題的方法。
DLL編碼約定簡(jiǎn)述
下面是我搜集到的所有的解決方案,其中一些是從網(wǎng)上的文章中拿來的,一些是跟不同的開發(fā)者交流后得到的。
下面的約定主要針對(duì)DLL開發(fā),而且是為解決DLL的向后兼容性問題:
1. 編碼約定:
?。?)DLL的每個(gè)導(dǎo)出類(或者它的父類)至少包含一個(gè)虛函數(shù)。這樣,這個(gè)類就會(huì)始終保存一個(gè)指向虛函數(shù)表的指針成員。這么做可以方便后來新的虛函數(shù)
的加入。
(2)如果你要給一個(gè)類增加一個(gè)虛函數(shù),那么將它加在所有其它虛函數(shù)的后面。這樣就不會(huì)改變虛函數(shù)表中原有函數(shù)的地址映射順序。
(3)如果你打算以后給一個(gè)類擴(kuò)充類成員,那么現(xiàn)在預(yù)留一個(gè)指向一個(gè)數(shù)據(jù)結(jié)構(gòu)的指針。這樣的話,增加一個(gè)成員直接在這個(gè)數(shù)據(jù)結(jié)構(gòu)中修改,而不是在類中修
改。于是,新成員的加入不會(huì)導(dǎo)致類尺寸的改變。當(dāng)然,為了訪問新成員,需要給這個(gè)類定義幾個(gè)操作函數(shù)。這種情況下,DLL必須是被客戶程序隱式
(implicitly)連接的。
(4)為了解決前一點(diǎn)的問題,也可以給所有的導(dǎo)出類設(shè)計(jì)一個(gè)純接口的類,但此時(shí),客戶程序?qū)o法從這些導(dǎo)出類繼續(xù)派生,DLL導(dǎo)出類的層次機(jī)構(gòu)也將無
法維持。
(5)發(fā)布兩個(gè)版本的DLL和LIB文件(Debug版本和Release版本)。因?yàn)槿绻话l(fā)布Release版本,開發(fā)者將無法調(diào)試他們的程序,因
為Release版與Debug版使用了不同的堆(Heap)管理器,因而當(dāng)Debug版本的客戶程序釋放Release版本DLL申請(qǐng)的內(nèi)存時(shí),會(huì)導(dǎo)致
運(yùn)行時(shí)錯(cuò)誤(Runtime
failure)。有一種辦法可以解決這個(gè)問題,就是DLL同時(shí)提供申請(qǐng)和釋放內(nèi)存的函數(shù)供客戶程序調(diào)用;DLL中也保證不釋放客戶程序申請(qǐng)的內(nèi)容。通常
遵守這個(gè)約定不是那么簡(jiǎn)單!
?。?)在編譯的時(shí)候,不要改變DLL導(dǎo)出類函數(shù)的默認(rèn)參數(shù),如果這些參數(shù)將被傳遞到客戶程序的話。
(7)注意內(nèi)聯(lián)(inline)函數(shù)的更改。
?。?)檢查所有的枚舉沒有默認(rèn)的元素值。因?yàn)楫?dāng)增加/刪除一個(gè)新的枚舉成員,你可能移動(dòng)舊枚舉成員的值。這就是為什么每一個(gè)成員應(yīng)該擁有一個(gè)唯一標(biāo)識(shí)
值。如果枚舉可以被擴(kuò)展,也應(yīng)該對(duì)其進(jìn)行文檔說明。這樣,客戶程序開發(fā)者就會(huì)引起注意。
?。?)不要改變DLL提供的頭文件中定義的宏。 2.
對(duì)DLL進(jìn)行版本控制:如果主要的DLL發(fā)生了改變,最好同時(shí)將DLL文件的名字也改掉,就象微軟的MFC
DLL一樣。例如,DLL文件可以按照如下格式命名:Dll_name_xx.dll,其中xx就是DLL的版本號(hào)。有時(shí)候DLL中做了很大的改動(dòng),使得
向后兼容性問題無法解決。此時(shí)應(yīng)該生成一個(gè)全新的DLL。將這個(gè)新DLL安裝到系統(tǒng)時(shí),舊的DLL仍然保留。于是,舊的客戶程序仍然能夠使用舊的DLL,
而新的客戶程序(使用新DLL編譯、連接)可以使用新的DLL,兩者互不干涉。
3. DLL的向后兼容性測(cè)試:還有很多很多中可能會(huì)破壞DLL的向后兼容性,因此實(shí)施DLL的向后兼容性測(cè)試是非常必要的!
接下去,我將來討論一個(gè)虛函數(shù)的問題,以及對(duì)應(yīng)的一個(gè)解決方案。
虛函數(shù)與繼承
首先來看一下如下的虛函數(shù)和繼承結(jié)構(gòu):
/**********DLL導(dǎo)出的類 **********/
class EXPORT_DLL_PREFIX VirtFunctClass{
public:
VirtFunctClass(){}
~VirtFunctClass(){}
virtual void DoSmth(){
//this->DoAnything();
// Uncomment of this line after the corresponding method
//will be added to the class declaration
}
//virtual void DoAnything(){}
// Adding of this virtual method will make shift in
// table of virtual methods
};
/**********客戶程序,從DLL導(dǎo)出類派生一個(gè)新的子類**********/
class VirtFunctClassChild : public VirtFunctClass {
public:
VirtFunctClassChild() : VirtFunctClass (){}
~VirtFunctClassChild(){};
virtual void DoSomething(){}
};
假設(shè)上面的兩個(gè)類,VirtFunctClass在my.dll中實(shí)現(xiàn),而VirtFunctClassChild在客戶程序中實(shí)現(xiàn)。接下去,我們做
一些改變,將如下兩個(gè)注釋行放開:
//virtual void DoAnything(){}
和
//this->DoAnything();
也就是說,DLL導(dǎo)出的類作了改動(dòng)!現(xiàn)在如果客戶程序沒有重新編譯,那么客戶程序中的VirtFunctClassChild將不知道DLL中
VirtFunctClass類已經(jīng)改變了:增加了一個(gè)虛函數(shù)void
DoAnything()。因此,VirtFunctClassChild類的虛函數(shù)表仍然包含兩個(gè)函數(shù)的映射:
1. void DoSmth()
2. void DoSomething()
而事實(shí)上這已經(jīng)不對(duì)了,正確的虛函數(shù)表應(yīng)該是:
1. void DoSmth()
2. void DoAnything()
3. void DoSomething()
問題就在于,當(dāng)實(shí)例化VirtFunctClassChild之后,如果調(diào)用它的void
DoSmth()函數(shù),DoSmth()函數(shù)轉(zhuǎn)而要調(diào)用void
DoAnything()函數(shù),但此時(shí)基類VirtFunctClass只知道要調(diào)用虛函數(shù)表中的第二個(gè)函數(shù),而VirtFunctClassChild
類的虛函數(shù)表中的第二個(gè)函數(shù)仍然是void DoSomething(),于是問題就出來了!
另外,禁止在DLL的導(dǎo)出類的派生類
(上例中的VirtFunctClassChild)中增加虛函數(shù)也是于事無補(bǔ)的。因?yàn)?,如果VirtFunctClassChild類中沒有
virtual void DoSomething()函數(shù),基類中的void
DoAnything()函數(shù)(虛函數(shù)表中的第二個(gè)函數(shù))調(diào)用將會(huì)指向一個(gè)空的內(nèi)存地址(因?yàn)閂irtFunctClassChild類維持的虛函數(shù)表僅
僅維持有一個(gè)函數(shù)地址)。
現(xiàn)在可以看出,在DLL的導(dǎo)出類中增加虛函數(shù)是一個(gè)多么嚴(yán)重的問題!不過,如果虛函數(shù)是用來處理回調(diào)事件的,我們有辦法來解決這個(gè)問題。
COM及其它
現(xiàn)在可以看出,DLL的向后兼容性問題是一個(gè)很出名的問題。解決這些問題,不僅可以借助于一些約定,而且可以通過其它一些先進(jìn)的技術(shù),比如COM技
術(shù)。因此,如果你想擺脫“DLL Hell”問題,請(qǐng)使用COM技術(shù)或者其它一些合適的技術(shù)。
讓我們回到我接受的那個(gè)任務(wù)(我在本文開頭的地方講到的那個(gè)任務(wù))————解決一個(gè)使用DLL的產(chǎn)品的向后兼容性問題。
我對(duì)COM有些了解,因此我的第一個(gè)建議是使用COM技術(shù)來克服那個(gè)項(xiàng)目中的所有問題。但這個(gè)建議因?yàn)槿缦略蜃罱K被否決了:
1. 那個(gè)產(chǎn)品已經(jīng)在某個(gè)內(nèi)部層中有一個(gè)COM服務(wù)器。
2. 將一大堆接口類重寫到COM的形式,投入比較大。
3. 因?yàn)槟莻€(gè)產(chǎn)品是DLL庫,而且已經(jīng)有很多應(yīng)用程序在使用它了。因此,他們不想強(qiáng)制他們的客戶重寫他們的應(yīng)用程序。
換句話說,我被要求完成的任務(wù)是,以最小的代價(jià)來解決這個(gè)DLL向后兼容性問題。當(dāng)然,我應(yīng)該指出,這個(gè)項(xiàng)目最主要的問題在于增加新的成員和接口類上的
虛回調(diào)函數(shù)。第一個(gè)問題可以簡(jiǎn)單地通過在類聲明中增加一個(gè)指向一個(gè)數(shù)據(jù)結(jié)構(gòu)的指針來解決(這樣可以任意增加新的成員)。這種方法我在上面已經(jīng)提到過。但是
第二個(gè)問題,虛回調(diào)函數(shù)的問題是新提出的。因此,我提出了下面的最小代價(jià)、最有效的解決方法。
虛回調(diào)函數(shù)與繼承
然我們想象一下,我們有一個(gè)DLL,它導(dǎo)出了幾個(gè)類;客戶應(yīng)用程序會(huì)從這些導(dǎo)出類派生新的類,以實(shí)現(xiàn)虛函數(shù)來處理回調(diào)事件。我們想在DLL中做一個(gè)很小的
改動(dòng)。這個(gè)改動(dòng)允許我們將來可以給導(dǎo)出類“無痛地”增加新的虛回調(diào)函數(shù)。同時(shí),我們也不想影響使用當(dāng)前版本DLL的應(yīng)用程序。我們期望的就是,這些應(yīng)用程
序只有在不得已的時(shí)候才協(xié)同新版本的DLL進(jìn)行一次重新編譯。因此,我給出了下面的解決方案:
我們可以保留DLL導(dǎo)出類中的每個(gè)虛回
調(diào)函數(shù)。我們只需記住,在任何一個(gè)類定義中增加一個(gè)新的虛函數(shù),如果應(yīng)用程序不協(xié)同新版本的DLL重新編譯,將導(dǎo)致嚴(yán)重的問題。我們所做的,就是想要避免
這個(gè)問題。這里我們可以一個(gè)“監(jiān)聽”機(jī)制。如果在DLL導(dǎo)出類中定義并導(dǎo)出的虛函數(shù)被用作處理回調(diào),我們可以將這些虛函數(shù)轉(zhuǎn)移到獨(dú)立的接口中去。
讓我們來看下面的例子:
// 如果想要測(cè)試改動(dòng)過的DLL,請(qǐng)將下面的定義放開
//#define DLL_EXAMPLE_MODIFIED
#ifdef DLL_EXPORT
#define DLL_PREFIX __declspec(dllexport)
#else
#define DLL_PREFIX __declspec(dllimport)
#endif
/********** DLL的導(dǎo)出類 **********/
#define CLASS_UIID_DEF static short GetClassUIID(){return 0;}
#define OBJECT_UIID_DEF virtual short
GetObjectUIID(){return this->GetClassUIID();}
// 所有回調(diào)處理的基本接口
struct DLL_PREFIX ICallBack
{
CLASS_UIID_DEF
OBJECT_UIID_DEF
};
#undef CLASS_UIID_DEF
#define CLASS_UIID_DEF(X) public: static
short GetClassUIID(){return X::GetClassUIID()+1;}
// 僅當(dāng)DLL_EXAMPLE_MODIFIED宏已經(jīng)定義的時(shí)候,進(jìn)行接口擴(kuò)展
#if defined(DLL_EXAMPLE_MODIFIED)
// 新增加的接口擴(kuò)展
struct DLL_PREFIX ICallBack01 : public ICallBack
{
CLASS_UIID_DEF(ICallBack)
OBJECT_UIID_DEF
virtual void DoCallBack01(int event) = 0; // 新的回調(diào)函數(shù)
};
#endif // defined(DLL_EXAMPLE_MODIFIED)
class DLL_PREFIX CExample{
public:
CExample(){mpHandler = 0;}
virtual ~CExample(){}
virtual void DoCallBack(int event) = 0;
ICallBack * SetCallBackHandler(ICallBack *handler);
void Run();
private:
ICallBack * mpHandler;
};
很顯然,為了給擴(kuò)展DLL的導(dǎo)出類(增加新的虛函數(shù))提供方便,我們必須做如下工作:
1. 增加ICallBack * SetCallBackHandler(ICallBack *handler);函數(shù);
2. 在每個(gè)導(dǎo)出類的定義中增加相應(yīng)的指針;
3. 定義3個(gè)宏;
4. 定義一個(gè)通用的ICallBack接口。
為了演示給CExample類增加新的虛回調(diào)函數(shù),我在這里增加了一個(gè)ICallBack01接口的定義。很顯然,新的虛回調(diào)函數(shù)應(yīng)該加在新的接口
中。每次DLL更新都新增一個(gè)接口(當(dāng)然,每次DLL更新時(shí),我們也可以給一個(gè)類同時(shí)增加多個(gè)虛回調(diào)函數(shù))。
注意,每個(gè)新接口必須從上一個(gè)版本的接口繼承。在我的例子中,我只定義了一個(gè)擴(kuò)展接口ICallBack01。如果DLL再下個(gè)版本還要增加新的虛回調(diào)
函數(shù),我們可以在定義一個(gè)ICallBack02接口,注意ICallBack02接口要從ICallBack01接口派生,就跟當(dāng)初
ICallBack01接口是從ICallBack接口派生的一樣。
上面代碼中還定義了幾個(gè)宏,用于定義需要檢查接口版本的函數(shù)。例
如我們要為新接口ICallBack01增加新函數(shù)DoCallBack01,如果我們要調(diào)用ICallBack * mpHandler;
成員的話,就應(yīng)該在CExample類進(jìn)行一下檢查。這個(gè)檢查應(yīng)該如下實(shí)現(xiàn):
if(mpHandler != NULL &&
mpHandler->GetObjectUIID()>=ICallBack01::GetClassUIID()){
((ICallBack01 *) mpHandler)->DoCallBack01(2);
}
我們看到,新回調(diào)接口增加之后,在CExample類的實(shí)現(xiàn)中只需簡(jiǎn)單地插入新的回調(diào)調(diào)用。
現(xiàn)在你可以看出,我們上述對(duì)DLL的改動(dòng)并不會(huì)影響客戶應(yīng)用程序。唯一需要做的,只是在采用這種新設(shè)計(jì)后的第一個(gè)DLL版本(為DLL導(dǎo)出類增加了宏定
義、回調(diào)基本接口ICallBack、設(shè)置回調(diào)處理的SetCallBackHandler函數(shù),以及ICallBack接口的指針)發(fā)布后,應(yīng)用程序進(jìn)
行一次重編譯。(以后擴(kuò)展新的回調(diào)接口,應(yīng)用程序的重新編譯不是必需的?。?/p>
以后如果有人想要增加新的回調(diào)處理,他就可以通過增加新接
口的方式來實(shí)現(xiàn)(向上例中我們?cè)黾覫CallBack01一樣)。顯然,這種改動(dòng)不會(huì)引起任何問題,因?yàn)樘摵瘮?shù)的順序并沒有改變。因此應(yīng)用程序仍然以以前
的方式運(yùn)行。唯一你要注意的是,除非你在應(yīng)用程序中實(shí)現(xiàn)了新的接口,否則你就接收不到新增加的回調(diào)調(diào)用。
我們應(yīng)該注意到,DLL的用戶仍然能夠很容易與它協(xié)同工作。下面是客戶程序中的某個(gè)類的實(shí)現(xiàn)例子:
// 如果DLL_EXAMPLE_MODIFIED沒有定義,使用以前版本的DLL
#if !defined(DLL_EXAMPLE_MODIFIED)
// 此時(shí)沒有使用擴(kuò)展接口ICallBack01
class CClient : public CExample{
public:
CClient();
void DoCallBack(int event);
};
#else // !defined(DLL_EXAMPLE_MODIFIED)
// 當(dāng)DLL增加了新接口ICallBack01后,客戶程序可以修改自己的類
// (但不是必須的,如果他不想處理新的回調(diào)事件的話)
class CClient : public CExample, public ICallBack01{
public:
CClient();
void DoCallBack(int event);
// 聲明DoCallBack01函數(shù)(客戶程序要實(shí)現(xiàn)它,以處理新的回調(diào)事件)
// (DoCallBack01是ICallBack01接口新增加的虛函數(shù))
void DoCallBack01(int event);
};
總結(jié)
以上是生活随笔為你收集整理的DLL的向后兼容性问题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 威驰仪表盘tripA还有tripb都是什
- 下一篇: 博主:Redmi K30系列和小米10系