From:?
http://blog.csdn.net/pansiom/archive/2006/01/01/568096.aspx
摘 ? 要: 在《 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 ”函數,具體代碼如下:
[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 的創建
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 ,便彈出如下三個提示框:
?



第 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 :
/// <summary> /// 原型是 :HMODULE LoadLibrary(LPCTSTR lpFileName); /// </summary> /// <param name="lpFileName">DLL 文件名 </param> /// <returns> 函數庫模塊的句柄 </returns> [DllImport("kernel32.dll")] static extern IntPtr LoadLibrary(string lpFileName); /// <summary> /// 原型是 : FARPROC GetProcAddress(HMODULE hModule, LPCWSTR lpProcName); /// </summary> /// <param name="hModule"> 包含需調用函數的函數庫模塊的句柄 </param> /// <param name="lpProcName"> 調用函數的名稱 </param> /// <returns> 函數指針 </returns> [DllImport("kernel32.dll")] static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); /// <summary> /// 原型是 : BOOL FreeLibrary(HMODULE hModule); /// </summary> /// <param name="hModule"> 需釋放的函數庫模塊的句柄 </param> /// <returns> 是否已釋放指定的 Dll</returns> [DllImport("kernel32",EntryPoint="FreeLibrary",SetLastError=true)] static extern bool FreeLibrary(IntPtr hModule); /// <summary> /// Loadlibrary 返回的函數庫模塊的句柄 /// </summary> private IntPtr hModule=IntPtr.Zero; /// <summary> /// GetProcAddress 返回的函數指針 /// </summary> private IntPtr farProc=IntPtr.Zero; |
?
4.?????? 添加 LoadDll 方法,并為了調用時方便,重載了這個方法:
?
/// <summary> /// 裝載 Dll /// </summary> /// <param name="lpFileName">DLL 文件名 </param> 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 方法,并為了調用時方便,也重載了這個方法,方法的具體代碼及注釋如下:
/// <summary> /// 獲得函數指針 /// </summary> /// <param name="lpProcName"> 調用函數的名稱 </param> public void LoadFun(string lpProcName) { // 若函數庫模塊的句柄為空,則拋出異常 if(hModule==IntPtr.Zero) throw(new Exception(" 函數庫模塊的句柄為空 , 請確保已進行 LoadDll 操作 !")); // 取得函數指針 farProc = GetProcAddress(hModule,lpProcName); // 若函數指針,則拋出異常 if(farProc==IntPtr.Zero) throw(new Exception(" 沒有找到 :"+lpProcName+" 這個函數的入口點 ")); } /// <summary> /// 獲得函數指針 /// </summary> /// <param name="lpFileName"> 包含需調用函數的 DLL 文件名 </param> /// <param name="lpProcName"> 調用函數的名稱 </param> public void LoadFun(string lpFileName,string lpProcName) { // 取得函數庫模塊的句柄 hModule=LoadLibrary(lpFileName); // 若函數庫模塊的句柄為空,則拋出異常 if(hModule==IntPtr.Zero) throw(new Exception(" 沒有找到 :"+lpFileName+"." )); // 取得函數指針 farProc = GetProcAddress(hModule,lpProcName); // 若函數指針,則拋出異常 if(farProc==IntPtr.Zero) throw(new Exception(" 沒有找到 :"+lpProcName+" 這個函數的入口點 ")); } |
?
6.?????? 添加 UnLoadDll 及 Invoke 方法, Invoke 方法也進行了重載:
/// <summary> /// 卸載 Dll /// </summary> public void UnLoadDll() { FreeLibrary(hModule); hModule=IntPtr.Zero; farProc=IntPtr.Zero; } |
?
????????? Invoke 方法的第一個版本:
/// <summary> /// 調用所設定的函數 /// </summary> /// <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 是進行安全檢查 , 若不能通過 , 則拋出異常 if(hModule==IntPtr.Zero) throw(new Exception(" 函數庫模塊的句柄為空 , 請確保已進行 LoadDll 操作 !")); if(farProc==IntPtr.Zero) 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); break; 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 方法的第二個版本,它是調用了第一個版本的:
/// <summary> /// 調用所設定的函數 /// </summary> /// <param name="IntPtr_Function"> 函數指針 </param> /// <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(IntPtr IntPtr_Function,object[] ObjArray_Parameter,Type[] TypeArray_ParameterType,ModePass[] ModePassArray_Parameter,Type Type_Return) { // 下面 2 個 if 是進行安全檢查 , 若不能通過 , 則拋出異常 if(hModule==IntPtr.Zero) throw(new Exception(" 函數庫模塊的句柄為空 , 請確保已進行 LoadDll 操作 !")); 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 類實例:
/// <summary> /// 創建一個 dld 類對象 /// </summary> 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) ”,先后彈出的提示框如下:



????????? 這三個提示框所得出的結果說明了靜態變量 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 :
// 由于 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 :
public void UnLoadDll() {// 使 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) { try {// 判斷 MyAssembly 是否為空或 MyAssembly 的命名空間不等于要調用方法的命名空間,如果條件為真,就用 Assembly.Load 加載所需 DLL 作為程序集 if(MyAssembly==null||MyAssembly.GetName().Name!=Namespace) MyAssembly=Assembly.Load(LoadDll(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 System.Windows.Forms.MessageBox.Show(" 裝載出錯 !"); } } } catch(System.NullReferenceException e) { System.Windows.Forms.MessageBox.Show(e.Message); } return (object)0; } |
?
?
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# 程序中,在程序運行時才用某一算法進行解壓解密后才進行加載,所以即使用反編譯軟件,也只能得到一個資源文件,且這個資源文件是用一個復雜算法進行加密過的,不可能再次對資源文件中的內容進行反編譯,從而大大加強了代碼的保密性。
?
?
參考文獻:
[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 著 劉基誠 譯 , 中國電力出版社