編寫一個DLL時應當注意什麼#1

來源:互聯網
上載者:User

庫與代碼重用

1、靜態庫vs動態庫

靜態庫的優勢和劣勢

動態庫的優勢與問題

2、靜態C/C++運行庫 vs 動態C/C++運行庫 & manifest

3、關於動態庫介面設計

 

 

庫與代碼重用

 

對於像C,C++這樣的語言來說,庫是擴充語言功能的重要手段,對於項目來說,程式碼程式庫是節約開發時間,縮減成本,劃分項目模組以利於多人合作,並通過重用已有代碼而避免重複勞動的必要工具。開發一個稍具規模的項目,總免不了設計和劃分模組,如何設計和劃分,採取什麼方案解決問題,總要面臨諸多取捨。

最近跟動態庫/靜態庫/動態靜態CRT/MFC打了諸多交道。本文是對這段時間遇到的問題與思考的總結。

 

1. 靜態庫 vs. 動態庫

 

靜態庫本質上就是編譯好的目標代碼(使用vc的話,編譯.cpp檔案或者.c檔案之後會得到目標檔案.obj,編譯結束後,link程式會按照指定的要求將目標檔案連結成最終的輸出檔案——常見的有.lib/.dll/.exe)的一份合集。

相比於動態庫,靜態庫沒有入口函數,不能獨立執行,不能動態連結,一旦被連結到目標檔案中,靜態庫的程式碼片段/資料區段都會被合并到目標檔案當中去。這是靜態庫與動態庫的本質區別。

 

由於上述差別,那麼我們就很容易理解下面的情況:如果你在靜態庫中定義一個靜態或全域對象,最終該對象會被連結到目標檔案的資料區段中(目標檔案可能是某個dll也可能是exe)。

 

靜態庫的優勢在於,簡單易用,你無需考慮記憶體的申請與釋放問題,因為靜態庫的程式碼片段作為目標模組的一部分(EXE或DLL),在其中所調用的new/delete在連結階段會被重定位到該目標模組的CRT函數地址。

對應的,如果你使用DLL,由於DLL中會自行初始化一份CRT堆,因此它的記憶體申請與釋放是在有別於在EXE的CRT堆中進行的(兩個不同的CRT堆),如果你從DLL中申請一塊記憶體出來使用,最後也必須交還給DLL模組內部去釋放。否則會產生Heap Collision,在Debug模式下,你會收到一個系統位於malloc中的斷言,並瞭解到發生了什麼。*

此處的敘述存在錯誤,更正如下:

一個DLL或許會,或許不會擁有一份自行初始化的CRT堆,這取決於DLL是通過動態連結MSVCRT還是靜態連結LIBCMT。由於CRT是通過一個全域變數_crtheap維護著一個Heap(Windows環境下),並且會在CRT初始化的時候初始化CRT堆,因此,如果整個應用程式都是通過連結動態msvcrt,使用同一份crt,則不會有問題。但是如果有任一二進位模組連結了靜態crt,則會擁有一份自己的crt堆。嚴格來說,你應當小心的保證來自於某個CRT的堆記憶體最後也被交還給該CRT堆,否則會發生錯誤,在Debug模式下,當你調用free函數釋放來自於另一個CRT堆的記憶體時,會有一個_ASSERTE(_CrtIsValidHeapPointer(pUserData));宣告失敗,提醒你用錯了堆。

如果我們的DLL是連結了一個靜態CRT並且發布出去了,那麼我們還是應當保證所有來自於該DLL的記憶體都由該DLL回收。其原因如上述分析所言,是由於該DLL擁有一份自己的CRT堆。

 

再有,如果你的靜態庫A依賴於靜態庫B的標頭檔,而目標應用程式EXE依賴了靜態庫A,那麼你在連結靜態庫A的時候,也必須連結靜態庫B。否則會有靜態庫A中的某些符號無法被找到,從而導致連結失敗。而動態庫A如果依賴於一個靜態庫B,那麼你無需在依賴該動態庫的EXE中再次包含該靜態庫,因為該動態庫A已經將該靜態庫B連結到A模組當中了,在DLL A中已經有了一份,如果EXE模組沒有直接依賴於靜態庫B的話,那麼就無需在EXE中再次link該靜態庫了。

 

上述問題也通常是混用靜態庫和動態庫時可能遇到的一個問題:如果有一個靜態庫A,被動態庫B和EXE C都用到了,而EXE C又需要使用動態庫B,那麼這份A的代碼和全域資料就在動態庫B和EXE C中存在了兩份。第一,這是一種對空間的浪費;其次,這可能會引發問題,比如,當我在EXE C中new一份A中的某對象X的指標,並將該指標傳入到動態庫B中使用,表面上看似乎沒有問題,但是如果這個對象X引用了A庫中的全域/待用資料,就可能會引發問題,因為此時動態庫B中存在有一份A庫中的全域/待用資料,而EXE C中有另一份,如果這個全域/待用資料被X使用的話,那麼就會得到前後不一致的上下文環境。具體可能產生什麼問題要看具體的邏輯是如何組織的。在這種情況下,最好能將靜態庫A重新組織成一個動態庫,這樣就不存在多份代碼以及多套全域資料的問題了;如果不能這麼做,你可以根據具體的應用程式組織的形式,採取DLL匯出A庫介面,EXE使用DLL匯出介面去訪問A庫而不直接連結A庫本身的方法來變通,但通常這不是根本的解決方案。

 

另一方面,在DLL中可以包含自己的資源檔,而LIB中不能。因為DLL是一份完整的PE檔案,而LIB只是若干個OBJ打包。這並不是說,使用LIB的時候不能使用資源(RC),你可以將LIB中使用的RC檔案,resource.h都include到目標應用程式當中,只要沒有資源ID衝突就可以正常使用,就像是你在目標應用程式的資源當中定義了這些資源一樣。而DLL中的資源則隸屬於DLL自身,不會與EXE中的資源在ID號上產生衝突,只是在使用的時候還需要針對Resource做一些Handle上的設定(參考AfxSetResourceHandle),才能使得API訪問DLL中的資源而非EXE中的。如果要編寫一個帶有資源的庫模組,DLL確實是不二選擇。

 

2. 靜態&動態C/C++ runtime 以及 manifest

 

靜態C-runtime庫叫做LIBC.lib,我們通常使用多線程的版本叫LIBCMT.lib,debug下的版本叫LIBCMTD.lib。而動態C-runtime,在VC的環境下,我們使用的是MSVCRT.lib,以及debug下的MSVCRTD.lib(這兩個lib實際是兩個匯出符號檔案,實際的運行庫位於msvcrt.dll/msvcrtd.dll中(VC6),如果是VC2005的版本則是msvcr80.dll/msvcr80d.dll)

C++的runtime庫,靜態版的是LIBCPMT.lib/LIBCPMTD.lib。動態版的是MSVCPRT.lib以及MSVCPRTD.lib。(類似的,實際的運行庫位於<VC2005中>msvcp80.dll/msvcp80d.dll)

 

在第一點的對比中已經提到過,如果同時又DLL模組依賴一個靜態庫,而又有一個依賴該DLL模組的EXE還依賴這個靜態庫,那麼最終的記憶體中就會存在兩份該靜態庫的代碼以及全域資料。這一點對於C/C++運行庫也是一樣的。所需要注意的是,只要是基於C語言以及C++語言的程式,幾乎所有的都會用到C/C++運行庫,因此如果在規劃一個項目的時候,發現有多個dll模組,那麼最好是使用動態版的運行庫,以避免最終的記憶體中出現多份不必要的運行庫代碼。

 

再有一點是關於XP之後的系統中,以及在VC2005之後的編譯環境中,微軟添加的manifest機制。該機制是為瞭解決同名但版本錯誤的DLL載入問題而提出的。比如,我們手上有兩份VC編譯器,分別是不同的版本,其中攜帶的MSVCR80.dll也是不同的版本,但是都叫MSVCR80.dll,如果我們在應用程式安裝時,簡單粗暴的將系統目錄下替換成我們應用程式所依賴的MSVCR80.dll,那麼可能會造成以往的應用程式無法正確啟動並執行問題,因為他們可能依賴的是其他版本的MSVCR80.dll。

解決這個問題的途徑是將一個dll的運行環境,版本,以及校正值等資訊編碼到一個目錄名當中,而在安裝應用程式時,將該版本的dll放置到對應名字的目錄下,這樣儘管dll名稱一致,我們也可以找到所需要正確版本的dll。但是應用程式怎麼知道需要依賴哪個版本的dll呢?如果應用程式不知道,即使系統中有正確版本的dll,作業系統也不知道應該去載入哪一個dll。

因此manifest檔案就把該模組所依賴的模組都標明了,通過名稱,校正碼,版本等等資訊以確保所指明的模組不會有歧義(用記事本開啟一個manifest檔案,你就可以看到上述資訊),當該應用程式EXE或者DLL被作業系統載入時,系統就會根據該資訊去尋找對應模組,首先會在C:/Windows/WinSxS目錄下找(開啟這個目錄,你就能看到大量不同版本的CRT/CPRT/MFC/ATL等等運行庫),如果沒有找到那麼也會應用程式目前的目錄下找。

 

這就是為什麼使用VC2005編譯一個dll也好,一個exe也好,總會多產生一個manifest的檔案的原因。所以如果使用了動態版本的CRT運行庫,或者其他任何DLL,那麼最好就不要關閉manifest產生的編譯選項。

 

3. 關於動態庫介面設計

 

如果動態庫可能經常會更新版本,提供新的功能介面,那麼如果使得動態庫的升級無需改動依賴於他的可執行模組就是一個值得思考的問題。

具體要達到上述目標,需要保證的有以下幾點:

a. 動態庫中的類的記憶體布局不暴露給外界,也就是說動態庫提供給使用者的.h中,不應當有類的成員資訊。這是因為,如果類的內部資料修改了,增加了一個變數或者減少了,那麼這個類所佔用的記憶體大小也就修改了,倘若有exe使用上一版本的類定義,new了一個類對象,那麼一旦發生類定義修改的情況,這部分代碼就必須要重新編譯連結了。比較標準的做法是dll只向外界提供介面定義,而不提供實現定義,即提供的是一個類,但是類中只包含純虛函數,而沒有資料<這種做法有點類似於COM的思路>;或者是只提供一系列普通函數以及一個實作類別的指標,這種做法有一個好處,如果添加新的函數介面,也無需重新更新外部程式,但是如果使用虛函數介面則不然。

b. 動態庫中的類應當不給外界提供構造析構的能力,同樣是基於上述原因,如果外界可以new/delete該類,那麼當類的定義修改時就會出現不得不重新編譯使用該庫的應用程式的情況。倘若只通過dll中定義好的借口進行互動,則不存在這個問題。比如外部不能直接new一個對象,而是必須從一個dll匯出函數中擷取一個對象的指標,外部同樣不能delete這個對象,而是必須將之送入另一個匯出函數交給dll去刪除。這樣只要dll保持著兩個介面不變,更新dll時是無需更新依賴該dll的應用程式的。(一個比較好的做法是將dll提供外界的介面類的構造解構函式設定為private)

c. Dll應當儘可能使用動態crt,如果用到了mfc應當儘可能使用動態mfc。這是為了避免dll與外部模組中存在多份重複代碼的情況。同時如果有dll需要依賴的其他靜態庫,也應當儘可能使用該庫的動態版本。

 

p.s. 如果想瞭解更多的關於靜態庫,動態庫,C運行庫方面的知識,可以參考《程式員的自我修養——連結裝載與庫》一書。

p.s.2 果然如pongba老大所言,將自己認為沒有問題的知識寫下來是一次很好的學習過程,寫下本篇文章的過程中,我將原先認為明白的,但是實際並不見得就真的明白的問題,梳理清楚了很多。寫的過程有助於把原先大腦中下意識存在的假設顯現出來,在思維中再次加工,將原先不明確的概念搞清楚。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.