,

How to use DLL?

網路上能夠找到許多和DLL相關的教學。但我相信大家看了之後依然會存在許多疑問,或者想只要會用就好並不會去了解更深一層的意義。有時我會想: 別人所撰寫的API,我們是不是應該至少要去了解其原理或設計呢? 如果一直當個只懂Call API的Programmer,而沒去學習更多解決問題的方法,只要換個語言應該就和廢人無異了吧! 言歸正傳,今天這篇教學是特地寫給Albert兄的,教導大家DLL使用的兩種方法: 裝載期間動態連結執行期間動態連結。我以範例來說明,讓大家可以迅速了解該如何使用(千言萬語抵不過Example Code吧?)。
Download SourceCode

寫DLL相當簡單。如果是VC的用戶,直接透過Wizard產生出DDL的專案,基本的Skeleton都有了。其中的2個重點如下: 匯出函式宣告與DLL檔定義。
匯出的函式我們必須使用_declspec(dllexport)或_declspec(dllimport)宣告,目的是為了讓裝載期間動態連結能夠找到DLL對應的函式。

#ifdef MYDLLHELLOWORLD_EXPORTS
#define MYDLLHELLOWORLD_API __declspec(dllexport)
#else
#define MYDLLHELLOWORLD_API __declspec(dllimport)
#endif
 
// Declare export functions
MYDLLHELLOWORLD_API void showHelloWorldString( LPCTSTR s);
如果是在執行期間動態連結,如果不是使用__declspec(dllexport),我們就必須宣告def檔。VC中新增def的畫面如下:

接著將要匯出的函式加入def中:
LIBRARY	"DLLHelloWolrd"
EXPORTS
	showHelloWorldString @1
在編譯之後,會產生DLLHelloWolrd.dll與DLLHelloWolrd.lib,這兩個檔案在使用DLL時是所被需要的。

使用裝載期間動態連結的重點如下:

  1. lib放置專案目錄下。
  2. 宣告_declspec(dllexport) void showHelloWorldString( LPCTSTR s)_declspec(dllimport) void showHelloWorldString( LPCTSTR s)。一般會直接將DDL的header file include進來,目前看過third-party的library都會附上dll、lib與header file。
  3. 加入#pragma comment宣告我們所要載入的lib。如果這個lib不正確,在編譯時就會發生連結錯了。lib與dll相同也有32位元與64位元之區別。

#include "stdafx.h"
#include "DLLHelloWorld.h"
#include <atlbase.h> 
#include <iostream>
 
using namespace std;
 
#pragma comment(lib, "DLLHelloWolrd")
 
int _tmain(int argc, _TCHAR* argv[])
{
	LPCWSTR pw = W2T( L"Hello world!");
	showHelloWorldString(pw);
 
	system("pause");
	return 0;
}

使用執行期間動態連結的重點如下:

  1. 宣告欲呼叫之函式之function pointer,如typedef void (*PFNEXPORTFUNC)(LPCTSTR)
  2. LoadLibrary與FreeLibrary。使用DLL之前,必須使用LoadLibrary將它載入,如果不是傳路徑給檔而是使用檔名,它會有一定的搜尋路徑。如果Load有問題,可確認HMODULE是否為NULL,最後用完當然要釋放資源。
  3. 透過GetProcAddress取得函式在DLL中的執行位置並呼叫。如GetProcAddress( hModule, “showHelloWorldString”)showHelloWorldString為要呼叫的函式名稱。也可透過判斷取得結果是否為NULL去確認呼叫是否正確。

#include "stdafx.h"
#include <atlbase.h> 
 
using namespace std;
 
LPCWSTR hwdll = W2T(L"DLLHelloWolrd.dll");
typedef void (*PFNEXPORTFUNC)(LPCTSTR);
 
int _tmain(int argc, _TCHAR* argv[])
{
	HMODULE hModule = ::LoadLibrary(hwdll);
	if( hModule != NULL ){
 
		PFNEXPORTFUNC fnHelloWorldDLL = (PFNEXPORTFUNC)::GetProcAddress( hModule, "showHelloWorldString");
		if( fnHelloWorldDLL != NULL ){
			fnHelloWorldDLL( W2T( L"Hello world!"));
		} else {
			cout << "Init function failure!" << endl;
		}
 
		FreeLibrary(hModule);
	} else {
		cout << "Load " << hwdll << " failure!" << endl;
	}
	system("pause");
 
	return 0;
}

到底用哪一個方法好呢? 裝載期間動態連接的優點是: 我們不需要去宣告有的沒的函式指標,也不用擔心介面宣告錯誤的問題。 但缺點是: 如果DDL檔案一但不見,這隻程式會永遠起不來。執行期間動態連結的缺點是: 一堆有的沒的函式指標宣告,稍微打錯就ㄘㄨㄚˋ賽了。 但優點是: 由於是在執行期間載入,如果讀取不到會獲得一個NULL,程式設計人員也可以根據這種情況做些例外處置。舉例來說,支援32與64位元的程式,會載入32與64位元的DLL。假如它所使用的名稱如example32.dll與example64.dll,我們可以透過先讀取example32.dll是否成功,再決定是否讀取example64.dll。另外一種使用情況也許是對於功能的alternative選擇。

另外大家看完這篇文章後,可能還有以下疑問:

  1. dllexport與dllimport差在哪?
  2. showHelloWorldString @1的@1有何用途?

第一個問題經過Silvius的指導, dllexport與dllimport的函義如下:

  • 以上述的例子而言,DLL專案在前處理器會加入MYDLLHELLOWORLD_EXPORTS一同編譯。這將使得函式宣告為dllexport以做匯出之用。(前處理器的用途可參考這篇)
  • 而使用此DLL的Client,會使用dllimport宣告,將DLL函式做匯入之用。

第二個問題,可以參考此篇,是為了相容於舊系統使用序數的載入方式。