本文將有以下4個部分來講如何使用g++編譯調用dll的c++代碼。
1.如何調用dll
2.動態鏈接和靜態鏈接的區別
3.g++的編譯參數以及如何編譯調用dll的c++代碼
4.總結
1.如何調用dll
動態鏈接庫(Dynamic Link Library),簡稱DLL。DLL 是一個包含可由多個程序同時使用的代碼和數據的庫。它允許程序共享執行特殊任務所必需的代碼和其他資源,一般來說,DLL是一種磁盤文件,以.dll、.DRV、.FON、.SYS和許多以.EXE為擴展名的系統文件都可以是DLL。它由全局數據、服務函數和資源組成,在運行時被系統加載到調用進程的虛擬空間中,成為調用進程的一部分。
DLL的調用可以分為兩種:一種是隱式調用(需要.lib和.dll),一種是顯示調用(需要.dll)。
1.1 隱式調用
隱式加載就是在程序編譯的時候就將dll編譯到可執行文件中。實現隱式鏈接只要將導入函數關鍵字_declspec(dllimport)函數名等寫到應用程序相應的頭文件中就可以了。下面將通過一個例子來講解隱式鏈接調用Dlltest.dll庫中的Min函數。
首先新建一個項目為TestDll,在Dlltest.h、Dlltest.cpp文件中分別輸入如下代碼:
Dlltest.h的代碼如下:
//隱式加載Dlltest #pragma comment(lib,"Dlltest.lib") //聲明外部函數 extern "C"_declspec(dllimport) int Max(int a,int b); extern "C"_declspec(dllimport) int Min(int a,int b);
Dlltest.cpp的代碼如下:
#include<stdio.h> #include"Dlltest.h" int main() { int a; a=Min(1,2); printf("Min(1,2)result is %d\n",a); return 0; }
在生成Dlltest.exe文件之前,要先將Dlltest.dll和Dlltest.lib拷貝到debug同目錄(工程根目錄)下,也可以拷貝到windows的System目錄下。如果DLL使用的是def文件,要刪除Dlltest.h文件中關鍵字extern "C"。Dlltest.h文件中的關鍵字pragma commit是要Visual C++的編譯器在link時,鏈接到Dlltest.lib文件。當然,開發人員也可以不使用#pragma comment(lib,"Dlltest.lib")語句,而直接在工程的Setting->Link頁的Object/Moduls欄填入Dlltest.lib既可。
1.2 顯式調用
顯式加載是指在程序運行過程中,需要用到dll里的函數時,再動態加載dll到內存中,這種加載方式因為是在程序運行后再加載的,dll的維護更容易,使得程序如果需要更新,很多時候直接更新dll,而不用重新安裝程序。使用這種方式使應用程序在執行過程中隨時可以加載DLL文件,也可以隨時卸載DLL文件。
下面講一個通過顯式鏈接調用DLL中的Max函數的例子。
#include <stdio.h> #include <windows.h> int main() { typedef int(*pMax)(int a,int b); typedef int(*pMin)(int a,int b); HINSTANCE hDLL; int A; PMax Max; //動態加載Dlltest.dll文件 HDLL=LoadLibrary("Dlltest.dll"); Max=(pMax)GetProcAddress(hDLL,"Max"); A=Max(1,2); Printf("result is %d\n",A); //卸載Dlltest.dll文件; FreeLibrary(hDLL); }
在上例中使用類型定義關鍵字typedef,定義指向和DLL中相同的函數原型指針,然后通過LoadLibray()將DLL加載到當前的應用程序中並返回當前DLL文件的句柄,然后通過GetProcAddress()函數獲取導入到應用程序中的函數指針,函數調用完畢后,使用FreeLibrary()卸載DLL文件。
學習參考鏈接
DLLs in Visual C++:
https://docs.microsoft.com/en-us/previous-versions/1ez7dh12%28v%3dvs.140%29
Walkthrough: Creating and Using a Dynamic Link Library (C++):
https://docs.microsoft.com/en-us/previous-versions/ms235636%28v%3dvs.140%29
Using Run-Time Dynamic Linking:
https://docs.microsoft.com/zh-cn/windows/desktop/Dlls/using-run-time-dynamic-linking
2.動態鏈接和靜態鏈接的區別
2.1 靜態鏈接和動態鏈接
靜態鏈接方法:靜態鏈接庫格式如:#pragma comment(lib, "Dlltest.lib") ,靜態鏈接時,載入代碼就會把程序會用到的動態代碼或動態代碼的地址確定下來,將靜態庫中的函數和數據與應用程序的其他模塊一起生成可執行程序。
動態鏈接方法:動態鏈接方法使用LoadLibrary()、GetProcessAddress()和FreeLibrary()等函數來鏈接dll,動態鏈接方式並不能一開始就完成動態鏈接,而是直到真正調用動態庫代碼時,載入程序才計算(被調用的那部分)動態代碼的邏輯地址,然后等到某個時候,程序又需要調用另外某塊動態代碼時,載入程序又去計算這部分代碼的邏輯地址,所以,這種方式使程序初始化時間較短,但運行期間的性能比不上靜態鏈接的程序,但是生成的可執行程序比較小,不占內存。
2.2 靜態鏈接和動態鏈接的優缺點
靜態鏈接的優點 :
a. 代碼裝載速度快,執行速度比動態鏈接庫略快;
b. 只需保證在開發者的計算機中有正確的.lib文件,在以二進制形式發布程序時不需考慮在用戶的計算機上.lib文件是否存在及版本問題。
動態鏈接的優點:
a. 比較節省內存並能減少頁面交換;
b. DLL文件與EXE文件獨立,只要輸出接口不變(即名稱、參數、返回值類型和調用約定不變),更換DLL文件不會對EXE文件造成任何影響,因而極大地提高了可維護性和可擴展性;
c. 不同編程語言編寫的程序只要按照函數調用約定就可以調用同一個DLL函數;
d. 適用於大規模的軟件開發,使開發過程獨立、耦合度小,便於不同開發者和開發組織之間進行開發和測試。
兩者的不足之處:
(1) 使用靜態鏈接生成的可執行文件體積較大,包含相同的公共代碼,造成浪費;
(2) 使用動態鏈接的應用程序要確保它依賴的DLL模塊存在,如果使用載入時動態鏈接,程序啟動時發現DLL不存在,系統將終止程序並給出錯誤信息。而使用運行時動態鏈接,系統不會終止,但由於DLL中的導出函數不可用,程序會加載失敗;速度比靜態鏈接慢。當某個模塊更新后,如果新模塊與舊的模塊不兼容,那么那些需要該模塊才能運行的軟件,統統不能正常運行。
2.3 靜態庫與動態庫鏈接、執行時的搜索路徑順序
以下【】中加粗部分引用自https://blog.csdn.net/sunshixingh/article/details/52185307
【靜態庫鏈接時搜索路徑順序:
1. ld會去找g++命令中的參數-L
2. 再找g++的環境變量LIBRARY_PATH
3. 再找內定目錄 /lib /usr/lib /usr/local/lib 這是當初compile g++時寫在程序內的
動態鏈接時、執行時搜索路徑順序:
1. 編譯目標代碼時指定的動態庫搜索路徑
2. 環境變量LD_LIBRARY_PATH指定的動態庫搜索路徑
3. 配置文件/etc/ld.so.conf中指定的動態庫搜索路徑
4. 默認的動態庫搜索路徑/lib
5. 默認的動態庫搜索路徑/usr/lib】
有關環境變量:
LIBRARY_PATH環境變量:程序靜態鏈接庫文件搜索路徑
LD_LIBRARY_PATH環境變量:程序動態鏈接庫文件搜索路徑
3.如何使用g++編譯調用dll的c++程序
3.1 g++編譯參數
G++手冊:https://linux.die.net/man/1/g++
編譯分為3步,首先對源文件進行預處理,這個過程主要是處理一些#號定義的命令或語句(如宏、#include、預編譯指令#ifdef等),生成*.i文件;然后進行編譯,這個過程主要是進行詞法分析、語法分析和語義分析等,生成*.s的匯編文件;最后進行匯編,將對應的匯編指令翻譯成機器指令,生成可重定位的二進制目標文件。
使用g++編譯鏈接示例:
#預編譯,生成main.i文件 g++ -E main.c -o main.i #編譯,生成main.S文件 g++ -S main.i #匯編,生成main.o文件 g++ -c main.S #鏈接,生成可執行文件 g++ main.o -o main
g++ 命令的基本用法如下:
g++ [options] [filenames]
G++主要接受與GCC相同的選項。如果只想要編譯的一些階段,可以使用-x(或文件名后綴)告訴g++從哪里開始,可以使用選項-c、-S或-E告訴g++從哪里停止。注意,一些組合(例如,-x cpp-output-E)指示g++什么也不做。
下面【】中加粗部分的參數解釋是通過翻譯g++手冊:https://linux.die.net/man/1/g++得到:
【1)-E參數
-E 選項指示編譯器僅對輸入文件進行預處理。當使用這個選項時,預處理的輸出會被送到標准輸出而不存儲在文件中.-E會使g++在預處理階段停止,不會運行編譯器,輸出的預處理源代碼的形式,它會被發送到標准輸出,預處理階段g++會忽略掉不需要預處理的輸入文件。
2)-S參數
-S 選項會使g++停止在編譯后的階段。默認情況下,源文件的匯編程序文件名是通過將后綴.c、.i等替換為.s來創建的,對於指定的每個非匯編輸入文件,輸出的匯編代碼文件的格式。g++在使用-S選項時會忽略不需要編譯的輸入文件。
3)-c參數
-c 選項用來編譯或匯編源文件,但並不會鏈接,輸出的格式為每個源文件的對象文件的形式(.o)。默認情況下,源文件的目標文件名是通過將.c、.i、.s等后綴替換為.o來創建的。g++在使用-c選項會自動忽略未識別的輸入文件,認為不需要編譯或匯編。
4)-o參數
-o 選項用來為產生的可執行文件用指定的文件名,將輸出置於文件文件中。如果沒有指定-o,則缺省值生成可執行文件(.exe)。
5) -l參數(小寫L)和-L參數
-l參數用來指定程序鏈接的庫,-l參數緊接着就是庫名,例如庫名是test,他的庫文件名是libtest.so,很容易看出,把庫文件名的頭lib和尾.so去掉就是庫名了。
-L參數跟着的是庫文件所在的目錄名。舉個栗子:我們把libtest.so放在" /ddd/study "目錄下,那鏈接參數就是-L/ddd/study -ltest。
6)-I參數(大寫i)
-I參數是用來指定頭文件目錄,-I參數可以使用相對路徑。】
到這里就已經講完我在編譯c++程序常用的g++參數了。
3.2 生成一個dll
我們使用g++編譯參數編譯調用dll的c++程序,除了要了解常用的g++參數,如何調用dll,還需要學會如何生成一個dll,畢竟要有dll,才能去調用。接下來我簡要介紹一下如何在Dev c++下生成一個dll。
步驟如下:
(進行如下步驟,首先要確保電腦已經安裝了Dev c++)
打開Dev c++,點擊 “file—》new—》project”
選擇dll,我使用c語言,我將dll項目命名為test_dll。截圖如下:
點擊ok后Dev c++會自動生成一個dllmain.c和dll.h文件。里面有一個HelloWorld的示例。截圖如下:
在dll.h文件添加如下代碼:
DLLIMPORT int Add(int a,int b); DLLIMPORT int Multi(int a,int b);
dll.h的部分截圖如下所示:
在dllmain.c添加如下代碼:
DLLIMPORT int Add(int a,int b){ return a+b; } DLLIMPORT int Multi(int a,int b){ return a*b; }
dllmian.c部分截圖如下所示:
然后編譯生成對應的dll(注意生成是64位還是32位的,怎么注意生成的版本是多少位的,請注意下圖紅框部分。)
我生成了兩個版本的dll文件,test_dll_32.dll為32位版本的,test_dll_64.dll為64位版本的,截圖如下。一般情況下,編譯生成的.dll文件在你新建項目的目錄下,找dll文件到對應目錄找就行。
3.3使用g++編譯調用dll的c++程序
接下來簡單寫一個調用dll的程序。
新建一個文件名為demo.cpp,我調用的dll名稱為test_dll_64.dll(64位),使用的是動態鏈接方式。調用dll的c++代碼如下:
#include <Windows.h> #include<iostream> #include<stdlib.h> using namespace std; int main(){ typedef int(*FUNT)(int,int); HINSTANCE hDllInst = LoadLibrary("test_dll_64.dll"); cout<<"hDllInst:"<<hDllInst<<endl; int a=12,b=3; FUNT add=(FUNT) GetProcAddress(hDllInst, "Add"); printf("add(12,3) result is: %d \n",add(12,3)); return 0; }
使用g++編譯,按住win+R,打開cmd(你要確保已經配置g++的環境變量),輸入如下指令:
#預編譯,生成demo.i文件 g++ -E demo.cpp -o demo.i #編譯,生成demo.S文件 g++ -S demo.i #匯編,生成demo.o文件 g++ -c demo.S #鏈接,生成demo.exe文件 g++ demo.o -o demo.exe #運行demo.exe demo.exe
在cmd下運行顯示hDllInst:0 ,之后顯示demo.exe已停止工作,運行截圖如下所示:
之所以會出現demo.exe已停止工作,是由於loadlibrary返回值為0帶來的結果。
我百思不得其解,為什么loadlibrary的返回值為0呢?我仔細檢查代碼明明沒有錯誤,g++編譯指令也沒有錯誤。
后來我將代碼改為調用test_dll_32.dll(32位的)。
HINSTANCE hDllInst = LoadLibrary("test_dll_32.dll"); cout<<"hDllInst:"<<hDllInst<<endl;
demo.cpp的 完整代碼截圖如下:
重新使用g++編譯。按住win+r,輸入cmd,輸入如下命令:
#編譯,生成demo.o文件 g++ -c demo.cpp #鏈接,生成demo.exe文件 g++ demo.o -o demo.exe #運行demo.exe demo.exe
Cmd下運行demo.exe輸出為
hDllInst:0x6c040000(調用dll成功。)
add(12,3) result is: 15。
符合預期,能夠成功調用dll。
cmd運行截圖如下:
3.4 題外話
來一段小插曲(可以跳過)。
介紹一下visual studio和Dev c++的編譯器。雖然我們經常用VS和Dev c++來編譯C++程序,但是可能有些人並不知道它們的編譯器是什么,就比如我。接下來簡要介紹一下VS和Dev c++的編譯器。
VS的編譯器名稱為MSVC。MSVC官方文檔為:
msvc的編譯器cl.exe
msvc的鏈接器link.exe
Dev-C++的編譯器名稱為Mingw(包含了g++和gcc)
4.總結
我在g++下編譯了32位版本和64位版本的可執行程序,當編譯的可執行程序為64位版本的,而調用的dll為64位版本時,運行不會出錯。當編譯的可執行程序為32位版本的,而調用的dll為64位時,運行可執行程序會出現loadlibrary的返回值為0,可執行程序停止工作。這樣證明了當我們編譯的可執行程序和應用調用的dll位數不匹配時,編譯時沒有出現錯誤,但會出現運行時錯誤。
因為我在某項目中遇到了類似的問題,編譯的可執行代碼版本與dll位數不匹配引起的loadlibrary返回值為0以及可執行程序已停止工作的現象,當時我卻沒有發現出現錯誤的原因,阻礙了我的項目進展。所以寫此博客記錄一下,希望能幫到和我有類似情況的人。
最后,在此感謝導師、彩虹、邦哥和小霞的幫助。