天天看點

C#程式實作動态調用DLL的研究

C#程式實作動态調用DLL的研究

摘 要:在《csdn開發高手》2004年第03期中的《化功大法——将DLL嵌入EXE》一文,介紹了如何把一個動态連結庫作為一個資源嵌入到可執行檔案,在可執行檔案運作時,自動從資源中釋放出來,通過靜态加載延遲實作DLL函數的動态加載,程式退出後實作臨時檔案的自動删除,進而為解決“DLL Hell”提供了一種解決方案。這是一個很好的設計思想,而且該作者也用C++實作了,在Internet上也有相似的VB程式,但在某一技術論壇上提起這種設計方法時,有網友提出:“這種方法好是好,但就是啟動速度太慢”。這是因為程式啟動時實作DLL釋放,然後再加載釋放出來的DLL,這個過程會耗費一定的時間。鑒于此問題,經過思索,提出另一個設計方案:DLL作為資源檔案嵌入程式,但不需進行DLL釋放及其重新加載。本文就是對該設計方案的原理分析及使用C#程式設計來實作該設計方案。

關鍵詞:動态調用DLL,嵌入DLL,C#

正 文:

一、 DLL與應用程式

動态連結庫(也稱為DLL,即為“Dynamic Link Library”的縮寫)是Microsoft Windows最重要的組成要素之一,打開Windows系統檔案夾,你會發現檔案夾中有很多DLL檔案,Windows就是将一些主要的系統功能以DLL子產品的形式實作。

動态連結庫是不能直接執行的,也不能接收消息,它隻是一個獨立的檔案,其中包含能被程式或其它DLL調用來完成一定操作的函數(方法。注:C#中一般稱為“方法”),但這些函數不是執行程式本身的一部分,而是根據程序的需要按需載入,此時才能發揮作用。

DLL隻有在應用程式需要時才被系統加載到程序的虛拟空間中,成為調用程序的一部分,此時該DLL也隻能被該程序的線程通路,它的句柄可以被調用程序所使用,而調用程序的句柄也可以被該DLL所使用。在記憶體中,一個DLL隻有一個執行個體,且它的編制與具體的程式設計語言和編譯器都沒有關系,是以可以通過DLL來實作混合語言程式設計。DLL函數中的代碼所建立的任何對象(包括變量)都歸調用它的線程或程序所有。

下面列出了當程式使用 DLL 時提供的一些優點:[1]

1) 使用較少的資源

當多個程式使用同一個函數庫時,DLL 可以減少在磁盤和實體記憶體中加載的代碼的重複量。這不僅可以大大影響在前台運作的程式,而且可以大大影響其他在 Windows 作業系統上運作的程式。

2) 推廣子產品式體系結構

DLL 有助于促進子產品式程式的開發。這可以幫助您開發要求提供多個語言版本的大型程式或要求具有子產品式體系結構的程式。子產品式程式的一個示例是具有多個可以在運作時動态加載的子產品的計帳程式。

3) 簡化部署和安裝

當 DLL 中的函數需要更新或修複時,部署和安裝 DLL 不要求重建立立程式與該 DLL 的連結。此外,如果多個程式使用同一個 DLL,那麼多個程式都将從該更新或修複中獲益。當您使用定期更新或修複的第三方 DLL 時,此問題可能會更頻繁地出現。

二、 DLL的調用

每種程式設計語言調用DLL的方法都不盡相同,在此隻對用C#調用DLL的方法進行介紹。首先,您需要了解什麼是托管,什麼是非托管。一般可以認為:非托管代碼主要是基于win 32平台開發的DLL,activeX的元件,托管代碼是基于.net平台開發的。如果您想深入了解托管與非托管的關系與差別,及它們的運作機制,請您自行查找資料,本檔案在此不作讨論。

(一) 調用DLL中的非托管函數一般方法

首先,應該在C#語言源程式中聲明外部方法,其基本形式是:

[DLLImport(“DLL檔案”)]

修飾符 extern 傳回變量類型 方法名稱 (參數清單)

其中:

DLL檔案:包含定義外部方法的庫檔案。

修飾符: 通路修飾符,除了abstract以外在聲明方法時可以使用的修飾符。

傳回變量類型:在DLL檔案中你需調用方法的傳回變量類型。

方法名稱:在DLL檔案中你需調用方法的名稱。

參數清單:在DLL檔案中你需調用方法的清單。

注意:需要在程式聲明中使用System.Runtime.InteropServices命名空間。

DllImport隻能放置在方法聲明上。

DLL檔案必須位于程式目前目錄或系統定義的查詢路徑中(即:系統環境變量中Path所設定的路徑)。

傳回變量類型、方法名稱、參數清單一定要與DLL檔案中的定義相一緻。

若要使用其它函數名,可以使用EntryPoint屬性設定,如:

[DllImport("user32.dll", EntryPoint="MessageBoxA")]

static extern int MsgBox(int hWnd, string msg, string caption, int type);

其它可選的 DllImportAttribute 屬性:

CharSet 訓示用在入口點中的字元集,如:CharSet=CharSet.Ansi;

SetLastError 訓示方法是否保留 Win32"上一錯誤",如:SetLastError=true;

ExactSpelling 訓示 EntryPoint 是否必須與訓示的入口點的拼寫完全比對,如:ExactSpelling=false;

PreserveSig訓示方法的簽名應當被保留還是被轉換, 如:PreserveSig=true;

CallingConvention訓示入口點的調用約定, 如:CallingConvention=CallingConvention.Winapi;

此外,關于“資料封送處理”及“封送數字和邏輯标量”請參閱其它一些文章[2]。

C#例子:

1. 啟動VS.NET,建立一個項目,項目名稱為“Tzb”,模闆為“Windows 應用程式”。

2. 在“工具箱”的“ Windows 窗體”項中輕按兩下“Button”項,向“Form1”窗體中添加一個按鈕。

3. 改變按鈕的屬性:Name為 “B1”,Text為 “用DllImport調用DLL彈出提示框”,并将按鈕B1調整到适當大小,移到适當位置。

4. 在類視圖中輕按兩下“Form1”,打開“Form1.cs”代碼視圖,在“namespace Tzb”上面輸入“using System.Runtime.InteropServices;”,以導入該命名空間。

5. 在“Form1.cs[設計]”視圖中輕按兩下按鈕B1,在“B1_Click”方法上面使用關鍵字 static 和 extern 聲明方法“MsgBox”,将 DllImport 屬性附加到該方法,這裡我們要使用的是“user32.dll”中的“MessageBoxA”函數,具體代碼如下:

然後在“B1_Click”方法體内添加如下代碼,以調用方法“MsgBox”:

MsgBox(0," 這就是用 DllImport 調用 DLL 彈出的提示框哦! "," 挑戰杯 ",0x30);

6. 按“F5”運作該程式,并點選按鈕B1,便彈出如下提示框:

C#程式實作動态調用DLL的研究

(二) 動态裝載、調用DLL中的非托管函數

在上面已經說明了如何用DllImport調用DLL中的非托管函數,但是這個是全局的函數,假若DLL中的非托管函數有一個靜态變量S,每次調用這個函數的時候,靜态變量S就自動加1。結果,當需要重新計數時,就不能得出想要的結果。下面将用例子說明:

1. DLL的建立

1) 啟動Visual C++ 6.0;

2) 建立一個“Win32 Dynamic-Link Library”工程,工程名稱為“Count”;

3) 在“Dll kind”選擇界面中選擇“A simple dll project”;

4) 打開Count.cpp,添加如下代碼:

// 導出函數,使用“ _stdcall ” 标準調用

extern "C" _declspec(dllexport)int _stdcall count(int init);

int _stdcall count(int init)

{//count 函數,使用參數 init 初始化靜态的整形變量 S ,并使 S 自加 1 後傳回該值

static int S=init;

S++;

return S;

}

5) 按“F7”進行編譯,得到Count.dll(在工程目錄下的Debug檔案夾中)。

2. 用DllImport調用DLL中的count函數

1) 打開項目“Tzb”,向“Form1”窗體中添加一個按鈕。

2) 改變按鈕的屬性:Name為 “B2”,Text為 “用DllImport調用DLL中count函數”,并将按鈕B1調整到适當大小,移到适當位置。

3) 打開“Form1.cs”代碼視圖,使用關鍵字 static 和 extern 聲明方法“count”,并使其具有來自 Count.dll 的導出函數count的實作,代碼如下:

[DllImport("Count.dll")]

static extern int count(int init);

4) 在“Form1.cs[設計]”視圖中輕按兩下按鈕B2,在“B2_Click”方法體内添加如下代碼:

MessageBox.Show(" 用 DllImport 調用 DLL 中的 count 函數, /n 傳入的實參為 0 ,得到的結果是: "+count(0).ToString()," 挑戰杯 ");

MessageBox.Show(" 用 DllImport 調用 DLL 中的 count 函數, /n 傳入的實參為 10 ,得到的結果是: "+count(10).ToString()+"/n 結果可不是想要的 11 哦!!! "," 挑戰杯 ");

MessageBox.Show(" 所得結果表明: /n 用 DllImport 調用 DLL 中的非托管 /n 函數是全局的、靜态的函數!!! "," 挑戰杯 ");

5) 把Count.dll複制到項目“Tzb”的bin/Debug檔案夾中,按“F5”運作該程式,并點選按鈕B2,便彈出如下三個提示框:

C#程式實作動态調用DLL的研究
C#程式實作動态調用DLL的研究
C#程式實作動态調用DLL的研究

第1個提示框顯示的是調用“count(0)”的結果,第2個提示框顯示的是調用“count(10)”的結果,由所得結果可以證明“用DllImport調用DLL中的非托管函數是全局的、靜态的函數”。是以,有時候并不能達到我們目的,是以我們需要使用下面所介紹的方法:C#動态調用DLL中的函數。

3. C#動态調用DLL中的函數

因為C#中使用DllImport是不能像動态load/unload assembly那樣,是以隻能借助API函數了。在kernel32.dll中,與動态庫調用有關的函數包括[3]:

①LoadLibrary(或MFC 的AfxLoadLibrary),裝載動态庫。

②GetProcAddress,擷取要引入的函數,将符号名或辨別号轉換為DLL内部位址。

③FreeLibrary(或MFC的AfxFreeLibrary),釋放動态連結庫。

它們的原型分别是:

HMODULE LoadLibrary(LPCTSTR lpFileName);

FARPROC GetProcAddress(HMODULE hModule, LPCWSTR lpProcName);

BOOL FreeLibrary(HMODULE hModule);

現在,我們可以用IntPtr hModule=LoadLibrary(“Count.dll”);來獲得Dll的句柄,用IntPtr farProc=GetProcAddress(hModule,”_count@4”);來獲得函數的入口位址。

但是,知道函數的入口位址後,怎樣調用這個函數呢?因為在C#中是沒有函數指針的,沒有像C++那樣的函數指針調用方式來調用函數,是以我們得借助其它方法。經過研究,發現我們可以通過結合使用System.Reflection.Emit及System.Reflection.Assembly裡的類和函數達到我們的目的。為了以後使用友善及實作代碼的複用,我們可以編寫一個類。

1) dld類的編寫:

1. 打開項目“Tzb”,打開類視圖,右擊“Tzb”,選擇“添加”-->“類”,類名設定為“dld”,即dynamic loading dll 的每個單詞的開頭字母。

2. 添加所需的命名空間及聲明參數傳遞方式枚舉:

using System.Runtime.InteropServices; // 用 DllImport 需用此 命名空間

using System.Reflection; // 使用 Assembly 類需用此 命名空間

using System.Reflection.Emit; // 使用 ILGenerator 需用此 命名空間

在“public class dld”上面添加如下代碼聲明參數傳遞方式枚舉:

/// <summary>

/// 參數傳遞方式枚舉 ,ByValue 表示值傳遞 ,ByRef 表示址傳遞

/// </summary>

public enum ModePass

{

ByValue = 0x0001,

ByRef = 0x0002

3. 聲明LoadLibrary、GetProcAddress、FreeLibrary及私有變量hModule和farProc:

/// 原型是 :HMODULE LoadLibrary(LPCTSTR lpFileName);

/// <param name="lpFileName">DLL 檔案名 </param>

/// <returns> 函數庫子產品的句柄 </returns>

[DllImport("kernel32.dll")]

static extern IntPtr LoadLibrary(string lpFileName);

/// 原型是 : FARPROC GetProcAddress(HMODULE hModule, LPCWSTR lpProcName);

/// <param name="hModule"> 包含需調用函數的函數庫子產品的句柄 </param>

/// <param name="lpProcName"> 調用函數的名稱 </param>

/// <returns> 函數指針 </returns>

static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);

/// 原型是 : BOOL FreeLibrary(HMODULE hModule);

/// <param name="hModule"> 需釋放的函數庫子產品的句柄 </param>

/// <returns> 是否已釋放指定的 Dll</returns>

[DllImport("kernel32",EntryPoint="FreeLibrary",SetLastError=true)]

static extern bool FreeLibrary(IntPtr hModule);

/// Loadlibrary 傳回的函數庫子產品的句柄

private IntPtr hModule=IntPtr.Zero;

/// GetProcAddress 傳回的函數指針

private IntPtr farProc=IntPtr.Zero;

4. 添加LoadDll方法,并為了調用時友善,重載了這個方法:

/// 裝載 Dll

public void LoadDll(string lpFileName)

hModule=LoadLibrary(lpFileName);

if(hModule==IntPtr.Zero)

throw(new Exception(" 沒有找到 :"+lpFileName+"." ));

若已有已裝載Dll的句柄,可以使用LoadDll方法的第二個版本:

public void LoadDll(IntPtr HMODULE)

if(HMODULE==IntPtr.Zero)

throw(new Exception(" 所傳入的函數庫子產品的句柄 HMODULE 為空 ." ));

hModule=HMODULE;

5. 添加LoadFun方法,并為了調用時友善,也重載了這個方法,方法的具體代碼及注釋如下:

/// 獲得函數指針

public void LoadFun(string lpProcName)

{ // 若函數庫子產品的句柄為空,則抛出異常

throw(new Exception(" 函數庫子產品的句柄為空 , 請確定已進行 LoadDll 操作 !"));

// 取得函數指針

farProc = GetProcAddress(hModule,lpProcName);

// 若函數指針,則抛出異常

if(farProc==IntPtr.Zero)

throw(new Exception(" 沒有找到 :"+lpProcName+" 這個函數的入口點 "));

/// <param name="lpFileName"> 包含需調用函數的 DLL 檔案名 </param>

public void LoadFun(string lpFileName,string lpProcName)

{ // 取得函數庫子產品的句柄

// 若函數庫子產品的句柄為空,則抛出異常

6. 添加UnLoadDll及Invoke方法,Invoke方法也進行了重載:

/// 解除安裝 Dll

public void UnLoadDll()

FreeLibrary(hModule);

hModule=IntPtr.Zero;

farProc=IntPtr.Zero;

Invoke方法的第一個版本:

/// 調用所設定的函數

/// <param name="ObjArray_Parameter"> 實參 </param>

/// <param name="TypeArray_ParameterType"> 實參類型 </param>

/// <param name="ModePassArray_Parameter"> 實參傳送方式 </param>

/// <param name="Type_Return"> 傳回類型 </param>

/// <returns> 傳回所調用函數的 object</returns>

public object Invoke(object[] ObjArray_Parameter,Type[] TypeArray_ParameterType,ModePass[] ModePassArray_Parameter,Type Type_Return)

// 下面 3 個 if 是進行安全檢查 , 若不能通過 , 則抛出異常

throw(new Exception(" 函數指針為空 , 請確定已進行 LoadFun 操作 !" ) );

if(ObjArray_Parameter.Length!=ModePassArray_Parameter.Length)

throw(new Exception(" 參數個數及其傳遞方式的個數不比對 ." ) );

// 下面是建立 MyAssemblyName 對象并設定其 Name 屬性

AssemblyName MyAssemblyName = new AssemblyName();

MyAssemblyName.Name = "InvokeFun";

// 生成單子產品配件

AssemblyBuilder MyAssemblyBuilder =AppDomain.CurrentDomain.DefineDynamicAssembly(MyAssemblyName,AssemblyBuilderAccess.Run);

ModuleBuilder MyModuleBuilder =MyAssemblyBuilder.DefineDynamicModule("InvokeDll");

// 定義要調用的方法 , 方法名為“ MyFun ”,傳回類型是“ Type_Return ”參數類型是“ TypeArray_ParameterType ”

MethodBuilder MyMethodBuilder =MyModuleBuilder.DefineGlobalMethod("MyFun",MethodAttributes.Public| MethodAttributes.Static,Type_Return,TypeArray_ParameterType);

// 擷取一個 ILGenerator ,用于發送所需的 IL

ILGenerator IL = MyMethodBuilder.GetILGenerator();

int i;

for (i = 0; i < ObjArray_Parameter.Length; i++)

{// 用循環将參數依次壓入堆棧

switch (ModePassArray_Parameter[i])

case ModePass.ByValue:

IL.Emit(OpCodes.Ldarg, i);

break;

case ModePass.ByRef:

IL.Emit(OpCodes.Ldarga, i);

default:

throw(new Exception(" 第 " +(i+1).ToString() + " 個參數沒有給定正确的傳遞方式 ." ) );

if (IntPtr.Size == 4) {// 判斷處理器類型

IL.Emit(OpCodes.Ldc_I4, farProc.ToInt32());

else if (IntPtr.Size == 8)

IL.Emit(OpCodes.Ldc_I8, farProc.ToInt64());

else

throw new PlatformNotSupportedException();

IL.EmitCalli(OpCodes.Calli,CallingConvention.StdCall,Type_Return,TypeArray_ParameterType);

IL.Emit(OpCodes.Ret); // 傳回值

MyModuleBuilder.CreateGlobalFunctions();

// 取得方法資訊

MethodInfo MyMethodInfo = MyModuleBuilder.GetMethod("MyFun");

return MyMethodInfo.Invoke(null, ObjArray_Parameter);// 調用方法,并傳回其值

Invoke方法的第二個版本,它是調用了第一個版本的:

/// <param name="IntPtr_Function"> 函數指針 </param>

public object Invoke(IntPtr IntPtr_Function,object[] ObjArray_Parameter,Type[] TypeArray_ParameterType,ModePass[] ModePassArray_Parameter,Type Type_Return)

// 下面 2 個 if 是進行安全檢查 , 若不能通過 , 則抛出異常

if(IntPtr_Function==IntPtr.Zero)

throw(new Exception(" 函數指針 IntPtr_Function 為空 !" ) );

farProc=IntPtr_Function;

return Invoke(ObjArray_Parameter,TypeArray_ParameterType,ModePassArray_Parameter,Type_Return);

2) dld類的使用:

1. 打開項目“Tzb”,向“Form1”窗體中添加三個按鈕。Name 和Text屬性分别為 “B3”、“用LoadLibrary方法裝載Count.dll”,“B4”、“調用count方法”,“B5”、“解除安裝Count.dll”,并調整到适當的大小及位置。

2. 在“Form1.cs[設計]”視圖中輕按兩下按鈕B3,在“B3_Click”方法體上面添加代碼,建立一個dld類執行個體:

/// 建立一個 dld 類對象

private dld myfun=new dld();

3. 在“B3_Click”方法體内添加如下代碼:

myfun.LoadDll("Count.dll"); // 加載 "Count.dll"

myfun.LoadFun("_count@4"); // 調入函數 count, "_count@4" 是它的入口,可通過 Depends 檢視

4. “Form1.cs[設計]”視圖中輕按兩下按鈕B4,在“B4_Click”方法體内添加如下代碼:

object[] Parameters = new object[]{(int)0}; // 實參為 0

Type[] ParameterTypes = new Type[]{typeof(int)}; // 實參類型為 int

ModePass[] themode=new ModePass[]{ModePass.ByValue}; // 傳送方式為值傳

Type Type_Return = typeof(int); // 傳回類型為 int

// 彈出提示框,顯示調用 myfun.Invoke 方法的結果,即調用 count 函數

MessageBox.Show(" 這是您裝載該 Dll 後第 "+myfun.Invoke(Parameters,ParameterTypes,themode,Type_Return).ToString()

+" 次點選此按鈕。 "," 挑戰杯 ");

5. “Form1.cs[設計]”視圖中輕按兩下按鈕B5,在“B5_Click”方法體内添加如下代碼:

myfun.UnLoadDll();

6. 按“F5”運作該程式,并先點選按鈕B3以加載“Count.dll”,接着點選按鈕B4三次以調用3次“count(0)”,先後彈出的提示框如下:

C#程式實作動态調用DLL的研究
C#程式實作動态調用DLL的研究
C#程式實作動态調用DLL的研究

這三個提示框所得出的結果說明了靜态變量S 經初始化後,再傳入實參“0”也不會改變其值為“0”。

7. 點選按鈕B5以解除安裝“Count.dll”,再點選按鈕B3進行裝載“Count.dll”,再點選按鈕B4檢視調用了“count(0)”的結果:

C#程式實作動态調用DLL的研究

從彈出的提示框所顯示的結果可以看到又開始重新計數了,也就是實作了DLL的動态裝載與解除安裝了。

(三) 調用托管DLL一般方法

C# 調用托管DLL是很簡單的,隻要在“解決方案資料總管”中的需要調用DLL的項目下用滑鼠右擊“引用”,并選擇“添加引用”,然後選擇已列出的DLL或通過浏覽來選擇DLL檔案,最後需要用using 導入相關的命名空間。

(四) 動态調用托管DLL

C# 動态調用托管DLL也需要借助System.Reflection.Assembly裡的類和方法,主要使用了Assembly.LoadFrom。現在,用例子說明:

首先,啟動VS.NET,建立一個Visual C# 項目,使用的模闆為“類庫”,名稱為“CsCount”,并在類“Class1”中添加靜态整型變量S及方法count:

// 由于 static 不能修飾方法體内的變量,是以需放在這裡,且初始化值為 int.MinValue

static int S=int.MinValue;

public int count(int init)

{// 判斷 S 是否等于 int.MinValue ,是的話把 init 指派給 S

if(S==int.MinValue) S=init;

S++; //S 自增 1

return S; // 傳回 S

然後,打開項目“Tzb”,向“Form1”窗體中添加一個按鈕,Name屬性為“B6”,Text屬性為“用Assembly類來動态調用托管DLL”,調整到适當大小和位置,輕按兩下按鈕B6,轉入代碼視圖,先導入命名空間:using System.Reflection; 接着添加Invoke方法和B6_Click方法代碼:

private object Invoke(string lpFileName,string Namespace,string ClassName,string lpProcName,object[] ObjArray_Parameter)

Try { // 載入程式集

Assembly MyAssembly=Assembly.LoadFrom(lpFileName);

Type[] type=MyAssembly.GetTypes();

foreach(Type t in type)

{// 查找要調用的命名空間及類

if(t.Namespace==Namespace&&t.Name==ClassName)

{// 查找要調用的方法并進行調用

MethodInfo m=t.GetMethod(lpProcName);

if(m!=null)

object o=Activator.CreateInstance(t);

return m.Invoke(o,ObjArray_Parameter);

else MessageBox.Show(" 裝載出錯 !");

}//try

catch(System.NullReferenceException e)

MessageBox.Show(e.Message);

}//catch

return (object)0;

}// Invoke

“B6_Click”方法體内代碼如下:

// 顯示 count(0) 傳回的值

MessageBox.Show(" 這是您第 "+Invoke("CsCount.dll","CsCount","Class1","count",new object[]{(int)0}).ToString()+" 次點選此按鈕。 "," 挑戰杯 ");

最後,把項目“CsCount”的bin/Debug檔案夾中的CsCount.dll複制到項目“Tzb”的bin/Debug檔案夾中,按“F5”運作該程式,并點選按鈕B6三次,将會彈出3個提示框,内容分别是“這是您第 1次點選此按鈕。”、“這是您第 2次點選此按鈕。”、“這是您第 3次點選此按鈕。”,由此知道了靜态變量S在這裡的作用。

(五) C#程式嵌入DLL的調用

DLL檔案作為資源嵌入在C#程式中,我們隻要讀取該資源檔案并以“byte[]”傳回,然後就用“Assembly Load(byte[]);”得到DLL中的程式集,最後就可以像上面的Invoke方法那樣對DLL中的方法進行調用。當然不用上面方法也可以,如用接口實作動态調用,但DLL中必須有該接口的定義并且程式中也要有該接口的定義;也可用反射發送實作動态調用[4]。現在我隻對像上面的Invoke方法那樣對DLL中的方法進行調用進行讨論,為了以後使用友善及實作代碼的複用,我們可以結合上一個編寫一個類。

1) ldfs類的編寫:

在項目“Tzb”中建立一個名為ldfs的類,意為“load dll from resource”,請注意,在這個類中“resource”不隻是嵌入在EXE程式中的資源,它也可以是硬碟上任意一個DLL檔案,這是因為ldfs的類中的方法LoadDll有些特别,就是先從程式的内嵌的資源中查找需加載的DLL,如果找不到,就查找硬碟上的。

首先導入所需的命名空間:

using System.IO; // 對檔案的讀寫需要用到此命名空間

using System.Reflection; // 使用 Assembly 類需用此命名空間

using System.Reflection.Emit; // 使用 ILGenerator 需用此命名空間

聲明一靜态變量MyAssembly:

// 記錄要導入的程式集

static Assembly MyAssembly;

添加LoadDll方法:

private byte[] LoadDll(string lpFileName)

Assembly NowAssembly = Assembly.GetEntryAssembly();

Stream fs=null;

try

{// 嘗試讀取資源中的 DLL

fs = NowAssembly.GetManifestResourceStream(NowAssembly.GetName().Name+"."+lpFileName);

finally

{// 如果資源沒有所需的 DLL ,就檢視硬碟上有沒有,有的話就讀取

if (fs==null&&!File.Exists(lpFileName)) throw(new Exception(" 找不到檔案 :"+lpFileName));

else if(fs==null&&File.Exists(lpFileName))

FileStream Fs = new FileStream(lpFileName, FileMode.Open);

fs=(Stream)Fs;

byte[] buffer = new byte[(int) fs.Length];

fs.Read(buffer, 0, buffer.Length);

fs.Close();

return buffer; // 以 byte[] 傳回讀到的 DLL

添加UnLoadDll方法來解除安裝DLL:

{// 使 MyAssembly 指空

MyAssembly=null;

添加Invoke方法來進行對DLL中方法的調用,其原理大體上和“Form1.cs”中的方法Invoke相同,不過這裡用的是“Assembly.Load”,而且用了靜态變量MyAssembly來儲存已加載的DLL,如果已加載的話就不再加載,如果還沒加載或者已加載的不同現在要加載的DLL就進行加載,其代碼如下所示:

public object Invoke(string lpFileName,string Namespace,string ClassName,string lpProcName,object[] ObjArray_Parameter)

{// 判斷 MyAssembly 是否為空或 MyAssembly 的命名空間不等于要調用方法的命名空間,如果條件為真,就用 Assembly.Load 加載所需 DLL 作為程式集

if(MyAssembly==null||MyAssembly.GetName().Name!=Namespace)

MyAssembly=Assembly.Load(LoadDll(lpFileName));

{// 調用并傳回

System.Windows.Forms.MessageBox.Show(" 裝載出錯 !");

System.Windows.Forms.MessageBox.Show(e.Message);

2) ldfs類的使用:

1. 把CsCount.dll作為“嵌入的資源”添加到項目“Tzb”中。

2. 向“Form1”窗體中添加兩個按鈕,Name和Text屬性分别為“B7”、“ldfs.Invoke調用count”;“B8”、“UnLoadDll”,并将它們調整到适當大小和位置。

3. 打開“Form1.cs”代碼視圖,添加一個ldfs執行個體:

// 添加一個 ldfs 執行個體 tmp

private ldfs tmp=new ldfs();

4. 在“Form1.cs[設計]”視圖中輕按兩下按鈕B7,在“B1_Click”方法體内添加如下代碼:

// 調用 count(0), 并使用期提示框顯示其傳回值

MessageBox.Show(" 這是您第 "+tmp.Invoke("CsCount.dll","CsCount","Class1","count",new object[]{(int)0}).ToString()+" 次點選此按鈕。 "," 挑戰杯 ");

5. 在“Form1.cs[設計]”視圖中輕按兩下按鈕B7,在“B1_Click”方法體内添加如下代碼:

// 解除安裝 DLL

tmp.UnLoadDll();

6. “F5”運作該程式,并先點選按鈕B7三次,接着點選按鈕B8,最後再點選按鈕B7,此時發現又開始重新計數了,情況和“dld類的使用”類似,也就是也實作了DLL的動态裝載與解除安裝了。

說明:以上所用到的所有源代碼詳見附件1:Form1.cs、附件2:dld.cs、附件3:ldfs.cs、附件4:Count.cpp、附件5:Class1.cs。

三、 結 論

使用DLL有很多優點,如:節省記憶體和減少交換操作;開發大型程式時可以把某些子產品配置設定給程式員,程式員可以用任何一門他所熟悉的語言把該子產品編譯成DLL檔案,這樣可以提高代碼的複用,大大減輕程式員的工作量。當然DLL也有一些不足,如在提要中提及的問題。是以,如何靈活地調用DLL應該是每位程式員所熟知的。

C# 語言有很多優點,越來越多的人開始使用它來程式設計。但是,C#還有一些不足,如對不少的底層操作是無能為力的,隻能通過調用Win32 DLL 或C++等編寫的DLL;另外,一般認為C#程式的保密性不夠強,因為它容易被Reflector 反編譯而得到部分源碼,是以需要使用混合程式設計加強C#程式的保密性,而把DLL嵌入C#程式并實作動态調用的方法是比較理想的方法,因為可以把DLL檔案先用某一算法進行加密甚至壓縮後再作為資源檔案添加到C#程式中,在程式運作時才用某一算法進行解壓解密後才進行加載,是以即使用反編譯軟體,也隻能得到一個資源檔案,且這個資源檔案是用一個複雜算法進行加密過的,不可能再次對資源檔案中的内容進行反編譯,進而大大加強了代碼的保密性。

參考文獻:

[2] 《在 C# 中通過 P/Invoke 調用Win32 DLL》 Jason Clark ,

[3] 《深入分析Windows和Linux動态庫應用異同》劉世棟 楊林,

[4] 《C# 程式設計》 Jesse Liberty 著 劉基誠 譯,中國電力出版社