在可執行文件中嵌入動態鏈接庫


©本文轉自網絡,著作權歸原作者所有。

原文:《C#程序實現動態調用DLL的研究

作者:黃金海岸

由於本文創作時間較早(2006年9月20日)所以原文中所使用的技術會顯得較為陳舊。本人將此文重新排版,作為歸檔之用。目前已有大量動態調用、dynamic編程等技術可用,故本文內容只做參考之用。)

《化功大法——將DLL嵌入EXE》(2004年第3期《CSDN開發高手》——野比注)一文,介紹了如何把一個動態鏈接庫作為一個資源嵌入到可執行文件,在可執行文件運行時,自動從資源中釋放出來,通過靜態加載延遲實現DLL函數的動態加載,程序退出后實現臨時文件的自動刪除,從而為解決“DLL Hell”提供了一種解決方案。這是一個很好的設計思想,而且該作者也用C++實現了,在Internet上也有相似的VB程序,但在某一技術論壇上提起這種設計方法時,有網友提出:“這種方法好是好,但就是啟動速度太慢”。這是因為程序啟動時實現DLL釋放,然后再加載釋放出來的DLL,這個過程會耗費一定的時間。鑒於此問題,經過思索,提出另一個設計方案: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的方法進行介紹。首先,您需要了解什么是托管,什么是非托管。一般可以認為:非托管代碼主要是基於Win32平台開發的DLL,activeX的組件,托管代碼是基於.net平台開發的。如果您想深入了解托管與非托管的關系與區別,及它們的運行機制,請您自行查找資料,本文件在此不作討論。 

1、調用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”函數,具體代碼如下: 

[DllImport("user32.dll", EntryPoint = "MessageBoxA")] 
static extern int MsgBox(int hWnd, string msg, string caption, int type); 

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

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

6、按“F5”運行該程序,並點擊按鈕B1,便彈出如下提示框:

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

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

1、DLL的創建 

a) 啟動Visual C++ 6.0; 

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

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

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

1 // 導出函數,使用“ _stdcall ” 標准調用 
2 extern "C" _declspec(dllexport)int _stdcall count(int init); 
3 int _stdcall count(int init) 
4 {
5     //count 函數,使用參數 init 初始化靜態的整形變量 S ,並使 S 自加 1 后返回該值 
6     static int S=init; 
7     S++; 
8     return S; 
9 } 

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

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

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

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

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

[DllImport("Count.dll")] 
static extern int count(int init); 

d) 在“Form1.cs[設計]”視圖中雙擊按鈕B2,在“B2_Click”方法體內添加如下代碼: 

1 MessageBox.Show(" 用 DllImport 調用 DLL 中的 count 函數, 傳入的實參為 0 ,得到的結果是: " + count(0).ToString(), " 挑戰杯 "); 
2 MessageBox.Show(" 用 DllImport 調用 DLL 中的 count 函數, 傳入的實參為 10 ,得到的結果是: " + count(10).ToString() + " 結果可不是想要的 11 哦!!! ", " 挑戰杯 "); 
3 MessageBox.Show(" 所得結果表明: 用 DllImport 調用 DLL 中的非托管函數是全局的、靜態的函數!!! ", " 挑戰杯 ");

e) 把Count.dll復制到項目“Tzb”的binDebug文件夾中,按“F5”運行該程序,並點擊按鈕B2,便彈出如下三個提示框:

 

 

第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. 添加所需的命名空間及聲明參數傳遞方式枚舉: 

1 using System.Runtime.InteropServices; // 用 DllImport 需用此 命名空間 
2 using System.Reflection; // 使用 Assembly 類需用此 命名空間 
3 using System.Reflection.Emit; // 使用 ILGenerator 需用此 命名空間

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

1 /// 
2 /// 參數傳遞方式枚舉 ,ByValue 表示值傳遞 ,ByRef 表示址傳遞 
3 /// 
4 public enum ModePass 
5 { 
6     ByValue = 0x0001, 
7     ByRef = 0x0002 
8 }

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

 1 /// 
 2 /// 原型是 :HMODULE LoadLibrary(LPCTSTR lpFileName); 
 3 /// 
 4 /// 
 5 DLL 文件名 
 6 /// 函數庫模塊的句柄 
 7 [DllImport("kernel32.dll")] 
 8 static extern IntPtr LoadLibrary(string lpFileName); 
 9 /// 
10 /// 原型是 : FARPROC GetProcAddress(HMODULE hModule, LPCWSTR lpProcName); 
11 /// 
12 /// 
13 包含需調用函數的函數庫模塊的句柄 
14 /// 
15 調用函數的名稱 
16 /// 函數指針 
17 [DllImport("kernel32.dll")] 
18 static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); 
19 /// 
20 /// 原型是 : BOOL FreeLibrary(HMODULE hModule); 
21 /// 
22 /// 
23 需釋放的函數庫模塊的句柄 
24 /// 是否已釋放指定的 Dll 
25 [DllImport("kernel32", EntryPoint = "FreeLibrary", SetLastError = true)] 
26 static extern bool FreeLibrary(IntPtr hModule); 
27 /// 
28 /// Loadlibrary 返回的函數庫模塊的句柄 
29 /// 
30 private IntPtr hModule = IntPtr.Zero; 
31 /// 
32 /// GetProcAddress 返回的函數指針 
33 /// 
34 private IntPtr farProc = IntPtr.Zero;

4. 添加LoadDll方法,並為了調用時方便,重載了這個方法:

 1 /// 
 2 /// 裝載 Dll 
 3 /// 
 4 /// DLL 文件名 
 5 public void LoadDll(string lpFileName) 
 6 { 
 7     hModule = LoadLibrary(lpFileName); 
 8     if(hModule == IntPtr.Zero) 
 9         throw(new Exception(" 沒有找到 :" + lpFileName + "." )); 
10 }

©本文轉自網絡,著作權歸原作者所有。由野比<conmajia@gmail.com>重新排版。

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

1 public void LoadDll(IntPtr HMODULE) 
2 { 
3     if(HMODULE == IntPtr.Zero) 
4         throw(new Exception(" 所傳入的函數庫模塊的句柄 HMODULE 為空 ." )); 
5     hModule = HMODULE; 
6 }

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

 1 /// 
 2 /// 獲得函數指針 
 3 /// 
 4 /// 調用函數的名稱 
 5 public void LoadFun(string lpProcName) 
 6 {
 7     // 若函數庫模塊的句柄為空,則拋出異常 
 8     if(hModule==IntPtr.Zero) 
 9         throw(new Exception(" 函數庫模塊的句柄為空 , 請確保已進行 LoadDll 操作 !")); 
10     // 取得函數指針 
11     farProc = GetProcAddress(hModule, lpProcName); 
12     // 若函數指針,則拋出異常 
13     if(farProc==IntPtr.Zero) 
14         throw(new Exception(" 沒有找到 :" + lpProcName + " 這個函數的入口點 ")); 
15 } 
16 /// 
17 /// 獲得函數指針 
18 /// 
19 /// 包含需調用函數的 DLL 文件名 
20 /// 調用函數的名稱 
21 public void LoadFun(string lpFileName, string lpProcName) 
22 {
23     // 取得函數庫模塊的句柄 
24     hModule = LoadLibrary(lpFileName); 
25     // 若函數庫模塊的句柄為空,則拋出異常 
26     if(hModule==IntPtr.Zero) 
27         throw(new Exception(" 沒有找到 :" + lpFileName + "." )); 
28     // 取得函數指針 
29     farProc = GetProcAddress(hModule, lpProcName); 
30     // 若函數指針,則拋出異常 
31     if(farProc == IntPtr.Zero) 
32         throw(new Exception(" 沒有找到 :" + lpProcName + " 這個函數的入口點 ")); 
33 }

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

1 /// 
2 /// 卸載 Dll 
3 /// 
4 public void UnLoadDll() 
5 { 
6     FreeLibrary(hModule); 
7     hModule = IntPtr.Zero; 
8     farProc = IntPtr.Zero; 
9 }

Invoke方法的第一個版本: 

 1 /// 
 2 /// 調用所設定的函數 
 3 /// 
 4 /// 實參 
 5 /// 實參類型 
 6 /// 實參傳送方式 
 7 /// 返回類型 
 8 /// 返回所調用函數的 object 
 9 public object Invoke(object[] ObjArray_Parameter, Type[] TypeArray_ParameterType, ModePass[] ModePassArray_Parameter, Type Type_Return) 
10 { 
11     // 下面 3 個 if 是進行安全檢查 , 若不能通過 , 則拋出異常 
12     if(hModule == IntPtr.Zero) 
13         throw(new Exception(" 函數庫模塊的句柄為空 , 請確保已進行 LoadDll 操作 !")); 
14     if(farProc == IntPtr.Zero) 
15         throw(new Exception(" 函數指針為空 , 請確保已進行 LoadFun 操作 !" ) ); 
16     if(ObjArray_Parameter.Length != ModePassArray_Parameter.Length) 
17         throw(new Exception(" 參數個數及其傳遞方式的個數不匹配 ." ) ); 
18     // 下面是創建 MyAssemblyName 對象並設置其 Name 屬性 
19     AssemblyName MyAssemblyName = new AssemblyName(); 
20     MyAssemblyName.Name = "InvokeFun"; 
21     // 生成單模塊配件 
22     AssemblyBuilder MyAssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(MyAssemblyName, AssemblyBuilderAccess.Run); 
23     ModuleBuilder MyModuleBuilder = MyAssemblyBuilder.DefineDynamicModule("InvokeDll"); 
24     // 定義要調用的方法 , 方法名為“ MyFun ”,返回類型是“
25     Type_Return ”參數類型是“ TypeArray_ParameterType ” 
26 MethodBuilder MyMethodBuilder = MyModuleBuilder.DefineGlobalMethod("MyFun", MethodAttributes.Public | MethodAttributes.Static, Type_Return, TypeArray_ParameterType); 
27     // 獲取一個 ILGenerator ,用於發送所需的 IL 
28     ILGenerator IL = MyMethodBuilder.GetILGenerator(); 
29     int i; 
30     for (i = 0; i 
31     {
32         // 用循環將參數依次壓入堆棧 
33         switch (ModePassArray_Parameter) 
34         { 
35             case ModePass.ByValue: 
36                 IL.Emit(OpCodes.Ldarg, i); 
37                 break; 
38             case ModePass.ByRef: 
39                 IL.Emit(OpCodes.Ldarga, i); 
40                 break; 
41             default: 
42                 throw(new Exception("" + (i + 1).ToString() + " 個參數沒有給定正確的傳遞方式 ." ) ); 
43         } 
44     } 
45     if (IntPtr.Size == 4)
46     {
47         // 判斷處理器類型 
48         IL.Emit(OpCodes.Ldc_I4, farProc.ToInt32()); 
49     } 
50     else if (IntPtr.Size == 8) 
51     { 
52         IL.Emit(OpCodes.Ldc_I8, farProc.ToInt64()); 
53     } 
54     else 
55     { 
56         throw new PlatformNotSupportedException(); 
57     } 
58     IL.EmitCalli(OpCodes.Calli, CallingConvention.StdCall, Type_Return, TypeArray_ParameterType); 
59     IL.Emit(OpCodes.Ret);
60     // 返回值 
61     MyModuleBuilder.CreateGlobalFunctions(); 
62     // 取得方法信息 
63     MethodInfo MyMethodInfo = MyModuleBuilder.GetMethod("MyFun"); 
64     return MyMethodInfo.Invoke(null, ObjArray_Parameter);
65     // 調用方法,並返回其值 
66 }

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

 1 /// 
 2 /// 調用所設定的函數 
 3 /// 
 4 /// 函數指針 
 5 /// 實參 
 6 /// 實參類型 
 7 /// 實參傳送方式 
 8 /// 返回類型 
 9 /// 返回所調用函數的 object 
10 public object Invoke(IntPtr IntPtr_Function, object[] ObjArray_Parameter, Type[] TypeArray_ParameterType, ModePass[] ModePassArray_Parameter, Type Type_Return) 
11 { 
12     // 下面 2 個 if 是進行安全檢查 , 若不能通過 , 則拋出異常 
13     if(hModule == IntPtr.Zero) 
14         throw(new Exception(" 函數庫模塊的句柄為空 , 請確保已進行 LoadDll 操作 !")); 
15     if(IntPtr_Function == IntPtr.Zero) 
16         throw(new Exception(" 函數指針 IntPtr_Function 為空 !" ) ); 
17     farProc = IntPtr_Function; 
18     return Invoke(ObjArray_Parameter, TypeArray_ParameterType, ModePassArray_Parameter, Type_Return); 
19 }

2) dld類的使用: 

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

2. 在“Form1.cs[設計]”視圖中雙擊按鈕B3,在“B3_Click”方法體上面添加代碼,創建一個dld類實例: 

1 /// 
2 /// 創建一個 dld 類對象 
3 /// 
4 private dld myfun=new dld();

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

1 myfun.LoadDll("Count.dll");
2 // 加載 "Count.dll" 
3 myfun.LoadFun("");
4 // 調入函數 count, "" 是它的入口,可通過 Depends 查看

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

1 object[] Parameters = new object[]{(int)0}; // 實參為 0 
2 Type[] ParameterTypes = new Type[]{typeof(int)}; // 實參類型為 int 
3 ModePass[] themode = new ModePass[]{ModePass.ByValue}; // 傳送方式為值傳 
4 Type Type_Return = typeof(int); // 返回類型為 int 
5 // 彈出提示框,顯示調用 myfun.Invoke 方法的結果,即調用 count 函數
6 MessageBox.Show(" 這是您裝載該 Dll 后第 " + myfun.Invoke(Parameters,ParameterTypes,themode, Type_Return).ToString() + " 次點擊此按鈕。 "," 挑戰杯 ");

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

1 myfun.UnLoadDll(); 

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

 

 

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

7. 點擊按鈕B5以卸載“Count.dll”,再點擊按鈕B3進行裝載“Count.dll”,再點擊按鈕B4查看調用了“count(0)”的結果:

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

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

C# 調用托管DLL是很簡單的,只要在“解決方案資源管理器”中的需要調用DLL的項目下用鼠標右擊“引用”,並選擇“添加引用”,然后選擇已列出的DLL或通過瀏覽來選擇DLL文件,最后需要用using 導入相關的命名空間。 

(四) 動態調用托管DLL 

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

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

 1 // 由於 static 不能修飾方法體內的變量,所以需放在這里,且初始化值為 int.MinValue 
 2 static int S = int.MinValue; 
 3 public int count(int init) 
 4 {
 5     // 判斷 S 是否等於 int.MinValue ,是的話把 init 賦值給 S 
 6     if(S == int.MinValue)
 7         S = init; 
 8     S++; //S 自增 1 
 9     return S; // 返回 S 
10 }

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

 1 private object Invoke(string lpFileName, string Namespace, string ClassName, string lpProcName, object[] ObjArray_Parameter) 
 2 { 
 3     try { // 載入程序集 
 4         Assembly MyAssembly = Assembly.LoadFrom(lpFileName); 
 5         Type[] type = MyAssembly.GetTypes(); 
 6         foreach(Type t in type) 
 7         {
 8             // 查找要調用的命名空間及類 
 9             if(t.Namespace == Namespace && t.Name == ClassName) 
10             {
11                 // 查找要調用的方法並進行調用 
12                 MethodInfo m = t.GetMethod(lpProcName); 
13                 if(m != null) 
14                 { 
15                     object o = Activator.CreateInstance(t); 
16                     return m.Invoke(o, ObjArray_Parameter); 
17                 } 
18                 else
19                     MessageBox.Show(" 裝載出錯 !"); 
20             } 
21         } 
22     }//try 
23     catch(System.NullReferenceException e) 
24     { 
25         MessageBox.Show(e.Message); 
26     }//catch 
27     return (object)0; 
28 }// Invoke

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

1 // 顯示 count(0) 返回的值 
2 MessageBox.Show(" 這是您第 " + Invoke("CsCount.dll", "CsCount", "Class1", "count",new object[]{(int)0}).ToString() + " 次點擊此按鈕。 ", " 挑戰杯 ");

最后,把項目“CsCount”的binDebug文件夾中的CsCount.dll復制到項目“Tzb”的binDebug文件夾中,按“F5”運行該程序,並點擊按鈕B6三次,將會彈出3個提示框,內容分別是“這是您第 1次點擊此按鈕。”、“這是您第 2次點擊此按鈕。”、“這是您第 3次點擊此按鈕。”,由此知道了靜態變量S在這里的作用。

©本文轉自網絡,著作權歸原作者所有。由野比<conmajia@gmail.com>重新排版。

(五) 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,如果找不到,就查找硬盤上的。 
首先導入所需的命名空間: 

1 using System.IO; // 對文件的讀寫需要用到此命名空間 
2 using System.Reflection; // 使用 Assembly 類需用此命名空間 
3 using System.Reflection.Emit; // 使用 ILGenerator 需用此命名空間 

聲明一靜態變量MyAssembly: 

 1 // 記錄要導入的程序集 
 2 static Assembly MyAssembly; 
 3 添加LoadDll方法: 
 4 private byte[] LoadDll(string lpFileName) 
 5 { 
 6     Assembly NowAssembly = Assembly.GetEntryAssembly(); 
 7     Stream fs = null; 
 8     try 
 9     {
10         // 嘗試讀取資源中的 DLL 
11         fs =  NowAssembly.GetManifestResourceStream(NowAssembly.GetName().Name + "." + lpFileName); 
12     } 
13     finally 
14     {
15         // 如果資源沒有所需的 DLL ,就查看硬盤上有沒有,有的話就讀取 
16         if (fs == null && !File.Exists(lpFileName))
17             throw(new Exception(" 找不到文件 :" + lpFileName)); 
18         else if(fs == null && File.Exists(lpFileName)) 
19         { 
20             FileStream Fs = new FileStream(lpFileName, FileMode.Open); 
21             fs = (Stream)Fs; 
22         } 
23     } 
24     byte[] buffer = new byte[(int) fs.Length]; 
25     fs.Read(buffer, 0, buffer.Length); 
26     fs.Close(); 
27     return buffer; // 以 byte[] 返回讀到的 DLL 
28 } 

添加UnLoadDll方法來卸載DLL: 

1 public void UnLoadDll() 
2 {
3     // 使 MyAssembly 指空 
4     MyAssembly = null; 
5 } 

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

 1 public object Invoke(string lpFileName, string Namespace, string ClassName, string lpProcName, object[] ObjArray_Parameter) 
 2 { 
 3     try 
 4     {
 5         // 判斷 MyAssembly 是否為空或 MyAssembly 的命名空間不等於要調用方法的命名空間,如果條件為真,就用 Assembly.Load 加載所需 DLL 作為程序集 
 6         if(MyAssembly== null||MyAssembly.GetName().Name !=  Namespace) 
 7             MyAssembly = Assembly.Load(LoadDll(lpFileName)); 
 8         Type[] type  = MyAssembly.GetTypes(); 
 9         foreach(Type t in type) 
10         { 
11             if(t.Namespace == Namespace && t.Name == ClassName) 
12             { 
13                 MethodInfo m = t.GetMethod(lpProcName); 
14                 if(m!=null) 
15                 {
16                     // 調用並返回 
17                     object o = Activator.CreateInstance(t); 
18                     return m.Invoke(o, ObjArray_Parameter); 
19                 } 
20                 else 
21                     System.Windows.Forms.MessageBox.Show(" 裝載出錯 !"); 
22             } 
23         } 
24     } 
25     catch(System.NullReferenceException e) 
26     { 
27         System.Windows.Forms.MessageBox.Show(e.Message); 
28     } 
29     return (object)0; 
30 }

2) ldfs類的使用: 

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

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

3. 打開“Form1.cs”代碼視圖,添加一個ldfs實例: 

1 // 添加一個 ldfs 實例 tmp 
2 private ldfs tmp = new ldfs(); 

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

1 // 調用 count(0), 並使用期提示框顯示其返回值 
2 MessageBox.Show(" 這是您第 " + tmp.Invoke("CsCount.dll", "CsCount", "Class1", "count",new object[]{(int)0}).ToString() + " 次點擊此按鈕。 "," 挑戰杯 "); 

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

1 // 卸載 DLL 
2 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#程序中,在程序運行時才用某一算法進行解壓解密后才進行加載,所以即使用反編譯軟件,也只能得到一個資源文件,且這個資源文件是用一個復雜算法進行加密過的,不可能再次對資源文件中的內容進行反編譯,從而大大加強了代碼的保密性。

參考文獻:

[1]  引自:《什么是 DLL?》,網址: http://support.microsoft.com/default.aspx?scid=kb;zh-cn;815065

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

網址:http://www.microsoft.com/china/msdn/library/langtool/vcsharp/ousNET.mspx

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

網址:http://tech.ccidnet.com/art/302/20050919/336005_1.html

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

(完)

©本文轉自網絡,著作權歸原作者所有


免責聲明!

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



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