針對動態加載方式的C/C++動態鏈接庫編寫


0、前言
筆者為客戶提供C/C++動態鏈接庫調用WEBSOCKET功能時,最初錯誤地認定客戶采用靜態加載的方式使用DLL庫,導致使用其它編程語言的客戶無法使用。考慮到為客戶服務常常要跨語言和跨IDE,最好的DLL庫的使用方式是動態調用,並且要減少DLL庫的依賴庫,避免對Windows下VS自帶庫的調用。本文針對動態調用提出一起DLL編寫注意事項。

1、靜態調用與動態調用
1.1 靜態調用
使用這種方式調用DLL庫的步驟(摘自網上url)為,youApp是你DLL的工程名,需要dll\lib\h頭文件:
①把你的youApp.DLL拷到你目標工程(需調用youApp.DLL的工程)的Debug目錄下;
②把你的youApp.lib拷到你目標工程(需調用youApp.DLL的工程)目錄下;
③把你的youApp.h(包含輸出函數的定義)拷到你目標工程(需調用youApp.DLL的工程)目錄下;
④打開你的目標工程選中工程,選擇Visual C++的Project主菜單的Settings菜單;
⑤執行第4步后,VC將會彈出一個對話框,在對話框的多頁顯示控件中選擇Link頁。然后在Object/library modules輸入框中輸入:youApp.lib
⑥選擇你的目標工程Head Files加入:youApp.h文件;
⑦最后在你目標工程(*.cpp,需要調用DLL中的函數)中包含你的:#include "youApp.h"
此種調用方式的優點是:DLL的函數名是通過.h文件和lib文件尋找到,實際的接口名可能和函數名不一致,但是不會導致無找不到入口的情況。它的缺點就是:非C/C++語言無法加載.h頭文件,跨語言時會遇到各種問題。
1.2 動態調用
動態調用的方法,先LoadLibrary,再GetProcAddress(即找到DLL中函數的地址),不用后FreeLibrary。具體示例代碼(摘自網上)如下:

{
    HINSTANCE hDllInst = LoadLibrary("youApp.DLL");
    if(hDllInst){
        typedef DWORD (WINAPI *MYFUNC)(DWORD,DWORD);
        MYFUNC youFuntionNameAlias = NULL; // youFuntionNameAlias 函數別名
        youFuntionNameAlias = (MYFUNC)GetProcAddress(hDllInst,"youFuntionName");
        // youFuntionName 在DLL中聲明的函數名
        if(youFuntionNameAlias){
            youFuntionNameAlias(param1,param2);
        }
    FreeLibrary(hDllInst);
    }
}

動態調用優點在於:不需要依賴.h和lib文件,更加便捷。缺點在於:生成DLL庫接口名需要和函數名一致,否則無法找到函數的入口點

2、DLL庫接口名和函數名的關系
2.1 接口名和函數名分析
如果接口名和函數名不一致找不到dll中的函數,出現“無法定位程序輸入點”的問題,如下圖所示。

對於DLL庫,查看它的接口名稱可以使用Depends工具,如需要可以聯系我。使用depends可以看到如下示例,在這個例子中Function的名稱即接口名稱,它和函數名稱是一致,這樣可以正常調用DLL。

而函數名稱與接口名不一致情況筆記忘了保存,舉例說明,sendMessage的Function name為_sendMessage@12,其中符號"_"和"@12"導致接口名和函數名不一致。這種情況使用在GetProcAddress函數中將函數名作為參數是無法找到函數入口點的。所以使用動態調用時,最好確保函數名和接口名的一致。
2.2 如何確保函數名和接口名一致
C++編譯器在生成DLL時,會對導出的函數進行名字改編,並且不同的編譯器使用的改編規則不一樣,因此改編后的名字也是不同的(一般涉及到C++ 中的重載等)。一般而言,生成dll有兩種方法,一是使用def文件,二是在函數定義前加_declspec(dllexport)。如果要導出C++文件中的函數,並且不讓編譯器改動函數名,用def文件導出函數並且在函數名前加extern "C"。
def的定義示例如下:

LIBRARY PrinterManager
EXPORTS
	initPrinterManager
	setRecvDataCallback
	sendMessage
	closePrinterManager

關於DLL導出名如下圖(來源於網上文章url),如果采用extern "C"和def則函數名和接口名一致,C++采用_declspec(dllexport)即函數名和接口名一致。其它情況函數名會被編譯器改編。


3、參數入棧順序(__stdcall和__cdecl),參考網上文章url
接口函數最好不用使用std中的容器,如string和vector等。這是因為計算機給這個函數傳遞參數,傳遞參數的工作必須由函數調用者和函數本身來協調,即函數傳遞參數的方式需要一致才能正確傳遞參數。計算機使用棧來支持參數傳遞,函數調用時,調用者依次把參數壓棧,然后調用函數,函數被調用以后,在堆棧中取得數據,並進行計算。函數計算結束以后,或者調用者、或者函數本身修改堆棧,使堆棧恢復原裝。
問題出現了,當參數個數多於一個時(數組為多個參數),按照什么順序把參數壓入堆棧,函數調用后,由誰來把堆棧恢復原裝 在高級語言中,通過函數調用約定來說明這兩個問題。常見的調用約定有:stdcall、cdecl、fastcall等,本文僅介紹stdcall和cdecl。
3.1 __stdcall
聲明方法:int __stdcall function(int a,int b)
__stdcall的調用約定意味着:1)參數從右向左壓入堆棧,2)函數自身修改堆棧 3)函數名自動加前導的下划線,后面緊跟一個@符號,其后緊跟着參數的尺寸。
在DLL的生成代碼中和使用代碼中都需要聲明__stdcall。跨語言推薦使用該方法
3.2 __cdecl
cdecl調用約定又稱為C調用約定,是C語言缺省的調用約定,它的定義語法是:
    int function (int a ,int b) //不加修飾就是C調用約定
    int __cdecl function(int a,int b)//明確指出C調用約定
參數采用從右到左的壓棧方式,傳送參數的內存棧由調用者維護。_cedcl約定的函數只能被C/C++調用,故跨語言不應該使用該方法。

4、減少DLL庫的依賴庫,避免對Windows下VS自帶庫的調用
如果對外提供的DLL庫使用VS自帶庫,那么其它語言很有可能就因為沒有VS自帶庫而無法運行。根據筆者的經驗,以下兩個步驟一定要使用來將減少DLL庫的依賴
4.1 步驟一:發布注意使用release方式而不能是debug
選擇方法如下圖所示:

4.2 步驟二:將項目中的MD改為MT
/MT是 "multithread, static version ” 意思是多線程靜態的版本,定義了它后,編譯器把LIBCMT.lib 安置到OBJ文件中,讓鏈接器使用LIBCMT.lib 處理外部符號。
/MD是 "multithread- and DLL-specific version” ,意思是多線程DLL版本,定義了它后,編譯器把MSVCRT.lib 安置到OBJ文件中,它連接到DLL的方式是靜態鏈接,實際上工作的庫是MSVCR80.DLL。
故采用MD的方式會使用額外的庫,如vcruntime140.dll或msvcp140.dll,而這兩庫在ISV處很可能是沒有的。
修改方法:
①打開項目的“屬性頁”對話框;
②展開“C/C++”文件夾;
③選擇“代碼生成”屬性頁;
④修改“運行庫”屬性。
如下圖所示:


5、總結
C/C++動態鏈接庫的使用充滿了復雜,很多新接觸者會無緣無故地陷入各種問題當中,故寫本文給看到的人以減少他們的彎路。特別感謝阿里實習同事的幫助,他們在我確決問題中給了我不少的指點。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM