C#程序實現動態調用DLL的研究 .

 

C#程序實現動態調用DLL的研究
 要:在《csdn開發高手》2004年第03期中的《化功大法——將DLL嵌入EXE》一文,介紹瞭如何把一個動態鏈接庫作爲一個資源嵌入到可執行文件,在可執行文件運行時,自動從資源中釋放出來,通過靜態加載延遲實現DLL函數的動態加載,程序退出後實現臨時文件的自動刪除,從而爲解決“DLL Hell”提供了一種解決方案。這是一個很好的設計思想,而且該作者也用C++實現了,在Internet上也有相似的VB程序,但在某一技術論壇上提起這種設計方法時,有網友提出:“這種方法好是好,但就是啓動速度太慢”。這是因爲程序啓動時實現DLL釋放,然後再加載釋放出來的DLL,這個過程會耗費一定的時間。鑑於此問題,經過思索,提出另一個設計方案:DLL作爲資源文件嵌入程序,但不需進行DLL釋放及其重新加載。本文就是對該設計方案的原理分析及使用C#編程來實現該設計方案。
 
關鍵詞:動態調用DLL,嵌入DLLC#
 
 文:
一、      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平臺開發的DLLactiveX的組件,託管代碼是基於.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”TextDllImport調用DLL彈出提示框,並將按鈕B1調整到適當大小,移到適當位置。
4.       在類視圖中雙擊“Form1”,打開“Form1cs”代碼視圖,在“namespace Tzb”上面輸入“using System.Runtime.InteropServices;”,以導入該命名空間。
5.       在“Form1cs[設計]”視圖中雙擊按鈕B1,在“B1_Click”方法上面使用關鍵字 static extern 聲明方法“MsgBox”,將 DllImport 屬性附加到該方法,這裏我們要使用的是“user32dll”中的“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調用DLLcount函數”,並將按鈕B1調整到適當大小,移到適當位置。
3)        打開“Form1cs”代碼視圖,使用關鍵字 static extern 聲明方法“count”,並使其具有來自 Count.dll 的導出函數count的實現,代碼如下:
 

[DllImport("Count.dll")]
static extern int count(int init);
4)        在“Form1cs[設計]”視圖中雙擊按鈕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(或MFCAfxFreeLibrary),釋放動態鏈接庫。
它們的原型分別是:
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.EmitSystem.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.       聲明LoadLibraryGetProcAddressFreeLibrary及私有變量hModulefarProc

/// <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.       添加UnLoadDllInvoke方法,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屬性爲“B6Text屬性爲“用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中方法的調用,其原理大體上和“Form1cs”中的方法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”窗體中添加兩個按鈕,NameText屬性分別爲“B7”、“ldfs.Invoke調用count”;“B8”、“UnLoadDll”,並將它們調整到適當大小和位置。
3. 打開“Form1cs”代碼視圖,添加一個ldfs實例:

// 添加一個 ldfs 實例 tmp
private ldfs tmp=new ldfs();
4. 在“Form1cs[設計]”視圖中雙擊按鈕B7,在“B1_Click”方法體內添加如下代碼:

// 調用 count(0), 並使用期提示框顯示其返回值
MessageBox.Show(" 這是您第 "+tmp.Invoke("CsCount.dll","CsCount","Class1","count",new object[]{(int)0}).ToString()+" 次點擊此按鈕。 "," 挑戰杯 ");
5. 在“Form1cs[設計]”視圖中雙擊按鈕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
[3] 《深入分析WindowsLinux動態庫應用異同》劉世棟楊林,
[4] C# 程序設計》 Jesse Liberty 劉基誠,中國電力出版社
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章