1. 程式人生 > >c++動態庫生成與呼叫

c++動態庫生成與呼叫

一、生成動態庫(含標頭檔案、不含標頭檔案)

以生成dllTest.dll為例(工程名為dllTest、 標頭檔案名為dllTest.h、 原始檔名為dllTest.cpp)

1.1 不含標頭檔案的動態庫

我們生成的動態庫想要被別人呼叫,那麼一定要將想要被呼叫的函式匯出,使用_declspec(dllexport)進行匯出。
//dllTest.cpp
_declspec(dllexport) int add(int a, int b)
{
	return a+b;
}

_declspec(dllexport) int sub(int a, int b)
{
	return a-b;
}

編譯之後,在工程目錄的Debug目錄下我們可以看到以下的幾個檔案,分別是
得到上述的檔案之後,我們就可以進行呼叫該動態連結庫了。其中,dll檔案是包含了函式具體實現的可執行檔案;lib檔案是匯入庫檔案,主要包含的是函式名和符號名。我們可以用vs提供的dumpbin工具檢視生成的動態庫中匯出的函式以及名字。我的vs安裝在C:\Program Files (x86)\Microsoft Visual Studio 10.0,進入該資料夾後點擊VC進入該目錄,可以看到一個vcvarsall.bat的檔案。依次點選Microsoft Visual Studio 2010 -> visual studio tools ->Visual Studio Prompt 2010,如下圖所示:
即可開啟一個vs的命令框,將vcvarsall.bat檔案拖拽進入該命令框中,將得到下圖所示的內容: 然後我們將得到的dllTest.dll檔案放入到VC資料夾中,輸入dumpbin -exports dllTest.dll即可看到該動態庫匯出的函式以及經過c++編譯器修飾後的函式名字。 橢圓所示的即為匯出的函式以及其經過修飾後的名字。(因為c++要支援函式過載以及名稱空間等,因此需要將函式進行修飾)。

1.2 動態庫呼叫(隱式連線、動態連線)

1.2.1 隱式連線

首先建立一個工程,配置工程屬性,具體的步驟如下: (1)新建一個win32控制檯應用程式,工程名dllCall; (2)配置工程屬性,新增對動態庫dllTest.lib的引用: a.  工程專案右鍵->屬性->連結器->gengeral->附加庫目錄,在其中新增匯入庫dllTest.lib所在的檔案目錄,如下圖:

b.工程專案右鍵->屬性->連結器->輸入->附加依賴項,在其中新增匯入庫dllTest.lib,如下圖:
(3)此時,我們就可以在我們的工程中對該動態庫進行呼叫了。 呼叫函式dllCal.cppl如下:
<pre name="code" class="cpp">// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
using namespace std;

extern int add(int, int );                             //告訴編譯器,add函式是在該原始檔外部定義的函式
_declspec(dllimport) int sub(int, int);//告訴編譯器,sub函式是從動態庫匯入的函式
//這兩種方式都可以正常的呼叫,但是下面的相對來說載入的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}


(4)編寫好之後,編譯、連結都能通過,但是執行的時候會出錯,因為呼叫函式不知道int add(int, int ) 以及int sub(int, int)的可執行檔案(dllTest.dll)在哪,因此當呼叫該函式時,就找不到具體的執行的程式碼,因而就會報錯。此時,我們只需將dllTest.dll放入到呼叫函式的.exe檔案所在的目錄中即可,結果如下圖: 上述的兩種形式都能正常的呼叫。

1.2.2 動態呼叫

上述的隱式呼叫需要我們在工程屬性中配置一些動態庫匯入庫(dllTest.lib)的目錄以及名稱,會很麻煩,有時候我們也會忘記配置這些屬性或者當動態庫較多的時候有遺漏,都會導致函式連結的時候出現unresolve external symbol的錯誤。而且,動態呼叫還有一個優點就是,什麼時候需要呼叫動態庫的函式的時候什麼時候載入該動態庫,這樣就不必在程式執行開始時載入所需的所有的動態庫,這樣也能加快啟動的速度。此外,我們僅僅只需要一個dllTest.dll檔案即可。 (1)我們首先刪掉工程屬性中 “附加庫目錄”以及“附加依賴項”中我們輸入的內容。 (2)修改相應的呼叫程式碼,如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include<Windows.h>
using namespace std;

//extern int add(int, int );                             //告訴編譯器,add函式是在該原始檔外部定義的函式
//_declspec(dllimport) int sub(int, int);//告訴編譯器,sub函式是從動態庫匯入的函式
//這兩種方式都可以正常的呼叫,但是下面的相對來說載入的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	HINSTANCE hInst = LoadLibraryA("dllTest.dll");
	typedef int (*pFun)(int, int);//定義一個函式指標型別pAdd
	pFun add = (pFun)GetProcAddress(hInst,"[email protected]@[email protected]");
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	pFun sub = (pFun)GetProcAddress(hInst,"[email protected]@[email protected]");
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}
(3)程式執行正常,結果同上面的結果。

2.含標頭檔案的動態庫

我們一般用編寫一個動態庫,同時也會提供一個頭檔案方便呼叫者使用,因為呼叫者一般情況下很難知道我們編寫的動態庫具體匯出的函式。而且,我們不想想上面的函式呼叫一樣,還要自己寫_declspec(dllimport) int add(int, int)或者extern int add(int, int);這是我們需要向呼叫者提供一個頭檔案完成這項工作,是呼叫變得更加方便。 標頭檔案:
//dllTest.h
#ifdef DLLTEST_API
#else 
#define DLLTEST_API _declspec(dllimport) 
#endif

DLLTEST_API int add(int, int);
DLLTEST_API int sub(int, int);
</pre><pre name="code" class="cpp"><span style="font-family: Arial, Helvetica, sans-serif;">	</span><span style="font-family: Arial, Helvetica, sans-serif;">原始檔:</span>
<pre name="code" class="cpp">//dllTest.cpp
#define DLLTEST_API _declspec(dllexport)
#include "dllTest.h"

DLLTEST_API int add(int a, int b)
{
	return a+b;
}

DLLTEST_API int sub(int a, int b)
{
	return a-b;
}


2.1 函式呼叫(隱式連結、動態連結)

2.1.1 隱式連結

(1) 新建工程,配置工程屬性(新增匯入庫目錄、匯入庫),具體的參照上面的隱式呼叫。此外,還要將動態庫標頭檔案所在的目錄加入到屬性中:  在  c\c++  ->  gengeral  ->  additional include Directories  加入dllTest.h所在的目錄。  如下圖: (2)修改呼叫函式,具體的程式碼如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include "dllTest.h"
using namespace std;

//extern int add(int, int );                             //告訴編譯器,add函式是在該原始檔外部定義的函式
//_declspec(dllimport) int sub(int, int);//告訴編譯器,sub函式是從動態庫匯入的函式
//這兩種方式都可以正常的呼叫,但是下面的相對來說載入的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}
(3)編譯、連結、執行正常,結果如上面。此外,我們也可以不用在屬性->連結器->輸入->附加庫目錄中新增dllTest.lib, 通過在呼叫函式中新增#pragma comment(lib,"dllTest.lib")即可。具體程式碼為:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include "dllTest.h"
#pragma comment(lib,"dllTest.lib")
using namespace std;

//extern int add(int, int );                             //告訴編譯器,add函式是在該原始檔外部定義的函式
//_declspec(dllimport) int sub(int, int);//告訴編譯器,sub函式是從動態庫匯入的函式
//這兩種方式都可以正常的呼叫,但是下面的相對來說載入的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}

2.1.2 動態連結

動態連結同上面的一樣。

3 不改變名字的匯出庫

但是我們發現,通過動態連結時,那個函式的名字經過c++編譯器修飾後很複雜,剛開始並不熟悉修飾的規則(後面講修飾規則)。因此,如果我們需要先用dumpbin工具查詢該動態庫匯出的函式及其名字,才方便呼叫。那麼,怎麼才能使匯出的函式名字不發生變化呢,同我們定義的函式名一樣,這樣我們在動態呼叫時更加方便。具體的方法有兩種:一是採用extern "C";二是用模組定義檔案.def。

3.1 extern "C"

3.1.1 預設呼叫方式_cedel

以含標頭檔案的動態庫為例進行修改,修改後的標頭檔案和原始檔如下:
//dllTest.h
#ifdef DLLTEST_API
#else 
#define DLLTEST_API extern "C" _declspec(dllimport) 
#endif

DLLTEST_API int add(int, int);
DLLTEST_API int sub(int, int);


//dllTest.cpp
#define DLLTEST_API extern "C" _declspec(dllexport)
#include "dllTest.h"

DLLTEST_API int add(int a, int b)
{
	return a+b;
}

DLLTEST_API int sub(int a, int b)
{
	return a-b;
}

進行編譯得到相應的.dll、.lib檔案,通過dumpbin工具,我們可以檢視匯出函式的名字,如下:
紅色部分為 沒有加extern "C" 匯出的函式名,它經過了c++編譯器的修飾。 綠色部分為添加了extern "C" 匯出的函式名,它的意思是告訴編譯器,以C的方式匯出函式。

3.1.2 更改呼叫方式 _stdcall 

但是如果改變了呼叫方式,通過這種方式進行匯出,函式的名字依舊會改變,我們採用_stdcall呼叫方式,也就是

WINAPI,後面講為什麼window API 函式都採用該種呼叫方式

(1)編寫_stdcall呼叫方式的動態庫(以含標頭檔案方式為例) 相應的標頭檔案和原始檔如下:
//dllTest.h
#ifdef DLLTEST_API
#else 
#define DLLTEST_API extern "C" _declspec(dllimport) 
#endif

DLLTEST_API int _stdcall add(int, int);
DLLTEST_API int _stdcall sub(int, int);

//dllTest.cpp
#define DLLTEST_API extern "C" _declspec(dllexport)
#include "dllTest.h"

DLLTEST_API int _stdcall add(int a, int b)
{
	return a+b;
}

DLLTEST_API int _stdcall sub(int a, int b)
{
	return a-b;
}
編譯後利用dumpbin工具檢視匯出函式的名字如下:
紅色的為採用c\c++預設呼叫方式匯出的函式名字; 綠色的為採用_stdcall 呼叫方式匯出的函式名字。至於為什麼會是這樣的名字在後面的名字修飾規則中進行說明。我們發現他的名字還是改變了。

3.2 模組定義檔案.def

3.2.1 預設呼叫方式(_cedel)動態庫

(1)新建一個Win32程式,選擇一個動態庫程式,勾選空工程。 (2)修改相應的程式碼(原始檔、模組定義檔案),具體的如下: 原始檔:
//dllTest.cpp
 int  add(int a, int b)
{
	return a+b;
}

int  sub(int a, int b)
{
	return a-b;
}

模組定義檔案:
LIBRARY dllTest

EXPORTS
add
sub
其中,LIBRARY 後面要與生成的動態庫的名稱相同,EXPORTS下面寫需要匯出的函式(add、sub),它會自動與你的原始檔中的函式進行匹配。也可以用 add1 = add、 sub1 = sub 這樣的方式來改變匯出函式的名字。
(3)通過dumpbin工具檢視動態庫的匯出函式,如下圖(分別是沒改名字以及改名字後的):

3.2.2 採用_stdcall,也就是WINAPI呼叫方式生成動態庫

(1) 原始檔
//dllTest.cpp
 int  _stdcall add(int a, int b)
{
	return a+b;
}

int  _stdcall sub(int a, int b)
{
	return a-b;
}

(2)模組定義檔案
LIBRARY dllTest

EXPORTS
add
sub

(3)利用dumpbin查詢動態庫匯出的函式

3.3 動態庫呼叫

3.3.1 動態呼叫

動態呼叫同上,只是變了一個地方,如下:
<span style="white-space:pre">	</span>pFun add = (pFun)GetProcAddress(hInst,"add");
<span style="white-space:pre">	</span>pFun sub = (pFun)GetProcAddress(hInst,"sub");
也可以使用每個函式前面的編號,如add函式的編號為1 、sub函式的編號為2。只需要將上述函式的第二個引數改為 MAKEINTATOM(1)

3.3.2 隱式呼叫

<1> 動態庫與函式庫都採用_cedel呼叫方式 (1) 配置工程屬性,加入附加庫目錄;(附加依賴項使用#pragma comment(lib,"testdll.lib"代替) (2)呼叫函式如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#pragma comment(lib,"testdll.lib")
using namespace std;


_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}
(3)編譯、連結正常,結果正確。 <2>動態庫函式採用_stdcall呼叫方式,呼叫方採用預設的呼叫方式(_cedel) (1)新建工程,配置工程屬性,加入附加庫目錄;(附加依賴項使用#pragma comment(lib,"testdll.lib"代替) (2)編寫呼叫函式,如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#pragma comment(lib,"testdll.lib")
using namespace std;


_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}
(3)編譯、連結正常,執行出錯。
這是因為_stdcall是被呼叫的函式自己清理棧空間,而_cedel則是呼叫者清理棧。上述的呼叫方式會使棧得到兩次清理,使得函式的返回地址、ebp暫存器的值被更改從而導致失敗。通過觀察其生成的組合語言就可以看到(下面的主要的,並不是全部):
main函式在呼叫add函式之後清理的棧,而在add函式中,可以清楚的看到,add函式在執行完之後自己清理的棧。
因此,導致棧資料出現錯誤。 c\c++編譯器修飾規則和函式呼叫中棧的變化後面有時間在寫!!!

相關推薦

c++動態生成呼叫

一、生成動態庫(含標頭檔案、不含標頭檔案) 以生成dllTest.dll為例(工程名為dllTest、 標頭檔案名為dllTest.h、 原始檔名為dllTest.cpp) 1.1 不含標頭檔案的動態庫 我們生成的動態庫想要被別人呼叫,那麼一定要將想要被呼叫的函式匯出,使用

C++ 動態生成

top pub rar start 找到 win 編譯 nbsp tro 目錄 一般創建方法 導出普通函數的方法&調用方法 導出類及其成員函數的方法&調用方法 眾所周知,我們可以將C++項目中的類以及函數導出,形成 .dll 文件,以

Linux動態生成以及呼叫

Linux下動態庫檔案的檔名形如 libxxx.so,其中so是 Shared Object 的縮寫,即可以共享的目標檔案。 在連結動態庫生成可執行檔案時,並不會把動態庫的程式碼複製到執行檔案中,而是在執行檔案中記錄對動態庫的引用。 程式執行時,再去載入動態庫檔案。如果動態庫已經載入,則不必重複

linux c/c++ 動態和靜態生成使用

二.介紹     從原始碼到可執行程式,通常要經過最重要的兩大步是:編譯,連結。編譯就是將原始檔生成中間檔案的過程,在linux下就是生成  .obj檔案。連結就是用連結器將,這些個中間檔案有序地”糅合“在一起,構成一個可執行檔案。通常,一個.c檔案或者.cpp原始檔編譯後,就會對應生成一個.obj檔案。  

Eclipse CDT生成呼叫C動態

(一) 生成動態庫 1 建立動態庫工程 File->New->Project->CProject->選擇Shared Library,工程命名為test。 2 建立原始碼檔案 File->New->Source File,指定名稱為te

g++ 編譯連結C++程式碼, 生成使用靜態動態

例如我有A.cpp、A.h、main.cpp 三個檔案 編譯連結C++程式碼: 第一步:g++ -c A.cpp main.cpp 這樣就可以編譯A.cpp和main.cpp的程式碼生成A.o和main.o檔案【因為A.cpp包含了A.h的標頭檔案,所以一般編譯時

2017.10.11 C#呼叫C++ 動態記憶體溢位問題

最近在寫C#呼叫C++ 動態庫的例子,發現使用Stringbuider 時,重複使用時容易造成記憶體溢位的問題,網上查了之後 發現可以使用 Stringbuider .Remove(0, Stringbuider .Length)方法,但是經過測試後並沒有解決問題。 後來經過仔細分析,

C++---動態靜態的區別

首先介紹一下靜態庫(靜態連結庫)、動態庫(動態連結庫)的概念,首先兩者都是程式碼共享的方式。 靜態庫:在連結步驟中,聯結器將從庫檔案取得所需的程式碼,複製到生成的可執行檔案中,這種庫稱為靜態庫,其特點是可執行檔案中包含了庫程式碼的一份完整拷貝;缺點就是被多次使用就會有多份冗餘拷貝。即靜態庫中的指

Delphi XE7呼叫C++動態出現亂碼問題

  事情源於有個客戶需使用我們C++的中介軟體動態庫來跟裝置連線通訊,但是傳入以及傳出的字串指標格式都不正確(出現亂碼或是被截斷),估計是字元編碼的問題導致。以下是解決問題的過程:   我們C++中介軟體動態庫的介面函式宣告: extern "C" bool __stdcall Exec

關於在electron中呼叫C++動態的經驗總結

前言       electron以nodejs作為底層執行環境,所以自然而然就想到了他能否呼叫C++編寫的動態庫,恰好我最近在做一個關於使用electron呼叫dll的專案,也就花了一點時間去了解和實踐,這期間走

GO語言生成C動態,再被連結成新動態

目錄 目標 測試程式碼 編譯步驟 GO ----> 動態庫 再封裝成新動態庫 編譯生成可執行檔案 嘗試GO---->C靜態庫---->C動態庫。 目標 如果想把GO語言實現的功能整合到C固定介面上,中間需要多加一次封裝。 因為很難用GO直

windows 下C++動態的封裝以及呼叫

1、一個程式從原始檔編譯生成可執行檔案的步驟:預編譯 -->  編譯 -->  彙編 --> 連結(1)預編譯,即預處理,主要處理在原始碼檔案中以“#”開始的預編譯指令,如巨集展開、處理條件編譯指令、處理#include指令等。(2)編譯過程就是把預處理完的檔案進行一系列

Go 生成C動態.so和靜態.a

Go 生成C動態庫.so和靜態庫.a 原始碼 package main import "C" import "fmt" //export hello func hello(){ fmt.Println("hello world") } //export add func

C#呼叫C/C++動態 封送結構體,結構體陣列

一. 結構體的傳遞 Cpp程式碼    #define JNAAPI extern "C" __declspec(dllexport) // C方式匯出函式   typedef struct       {         int osVersion;  

關於java jni呼叫c++動態的一些問題及解決方法

最近使用java jni介面技術呼叫c++完成的動態庫,平臺為虛擬機器下的centos 6.6。 編譯出來的*.so 檔案在被java呼叫過程中出現了各種錯誤。 1. java com.cmsz.znw.filevalmain.FileValServerImpl Exception in

JNA呼叫C動態dll、so

1.介紹jna           JNA(Java Native Access )提供一組Java工具類用於在執行期動態訪問系統本地庫(native library:如Window的dll)而不需要編寫任何Native/JNI程式碼。開發人員只要在一個java介面中描述目

Linux動態.a動態.so的生成區別、以及.so檔案的封裝使用

一、前言 如果有公司需要使用你們產品的一部分功能(通過程式碼呼叫這些功能),如果不想提供原始碼,那麼就可以通過封裝成庫檔案的形式提供給對方使用。本文主要介紹了生成動態庫與靜態庫檔案的過程、以及封裝和使用庫檔案的方法。 二、靜態庫.a與動態庫.so的生成與

C#呼叫C/C++動態,封裝各種複雜結構體。

    現在公司要做一個使用C#程式呼叫C++的一個DLL庫,解析檔案的功能。所以在網上找了一些資料。     一、結構體傳遞 #define JNAAPI extern "C" __declspec(dllexport) // C方式匯出函式 typedef str

linux環境下的c++ 動態呼叫

主要是為了平時的學習記錄,不妥的地方,煩請指點。一.下面主要是dlopen開啟動態庫.so相關的API介面函式。1. void* dlopen(const char* filename,int flag);filename 是動態庫的path路徑,flag是動態庫載入的幾種方

Linux C 靜態(.a) 動態(.so) 的詳解

庫從本質上來說是一種可執行程式碼的二進位制格式,可以被載入記憶體中執行。庫分靜態庫和動態庫兩種。 一、靜態庫和動態庫的區別 1、靜態函式庫 這類庫的名字一般是libxxx.a;利用靜態函式庫編譯成的檔案比較大--空間,因為整個函式庫的所有資料都會被整合進目的碼中,他的優點就顯而易見了,即編譯後的執行