C#程序实现动态调用DLL的研究
转自断点社区 http://bbs.C#程序实现动态调用DLL的研究
摘 要:开发高手》2004期中的《化功大法——将DLL》一文,介绍了如何把一个动态链接库作为一个资源嵌入到可执行文件,在可执行文件运行时,自动从资源中释放出来,通过静态加载延迟实现DLL函数的动态加载,程序退出后实现临时文件的自动删除,从而为解决“DLL Hell实现了,在Internet程序,但在某一技术论坛上提起这种设计方法时,有网友提出:“这种方法好是好,但就是启动速度太慢”。这是因为程序启动时实现DLL,这个过程会耗费一定的时间。鉴于此问题,经过思索,提出另一个设计方案:DLL释放及其重新加载。本文就是对该设计方案的原理分析及使用C#关键词:,嵌入DLL正 文:
一、 DLL与应用程序
,即为“Dynamic Link Library最重要的组成要素之一,打开Windows文件,Windows模块的形式实现。
调用来完成一定操作的函数(中一般称为“方法”)DLL也只能被该进程的线程访问,它的句柄可以被调用进程所使用,而调用进程的句柄也可以被该DLL只有一个实例,且它的编制与具体的编程语言和编译器都没有关系,所以可以通过DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。
时提供的一些优点:[1]
1) 可以减少在磁盘和物理内存中加载的代码的重复量。这不仅可以大大影响在前台运行的程序,而且可以大大影响其他在 Windows 2) DLL 3) 中的函数需要更新或修复时,部署和安装 DLL 的链接。此外,如果多个程序使用同一个 DLL时,此问题可能会更频繁地出现。
二、 DLL的调用
的方法都不尽相同,在此只对用C#的方法进行介绍。首先,什么是非托管。一般可以认为:非托管代码主要是基于win 32,activeX平台开发的。如果您想深入了解托管与非托管的关系与区别,及它们的运行机制,请您自行查找资料,本文件在此不作讨论。
(一) 调用DLL中的非托管函数一般方法
,应该在C#[DLLImport(“DLL返回变量类型 (参数列表)
:
DLL访问修饰符,除了abstract文件中你需调用方法的返回变量类型。
文件中你需调用方法的名称。
文件中你需调用方法的列表。
:需要在程序声明中使用System.Runtime.InteropServices DllImportDLL所设置的路径)。
文件中的定义相一致。
属性设置,如:
[DllImport("user32.dll", EntryPoint="MessageBoxA")]
static extern int MsgBox(int hWnd, string msg, string caption, int type);
属性:
CharSet ;
SetLastError ;
ExactSpelling 是否必须与指示的入口点的拼写完全匹配,如:ExactSpelling=falsePreserveSig如:PreserveSig=trueCallingConvention如:CallingConvention=CallingConvention.Winapi
。
C#1. ,新建一个项目,项目名称为“Tzb应用程序”。
2. 窗体”项中双击“Button”窗体中添加一个按钮。
3. 为 “B1”为 “调用DLL,并将按钮B14. ”,打开“Form1”代码视图,在“namespace Tzb”,以导入该命名空间。
5. .cs,在“B1_Click和 extern ”,将 DllImport .dll”函数,具体代码如下:
[DllImport("user32.dll", EntryPoint="MessageBoxA")]
static extern int MsgBox(int hWnd, string msg, string caption, int type);
”方法体内添加如下代码,以调用方法“MsgBox
MsgBox(0," 这就是用 DllImport 调用 DLL 弹出的提示框哦! "," 挑战杯 ",0x30);
6. ”,便弹出如下提示框:
(二) 动态装载、调用DLL中的非托管函数
调用DLL中的非托管函数有一个静态变量S就自动加11. DLL的创建
1) ;
2) 个“Win32 Dynamic-Link Library”;
3) ”选择界面中选择“A simple dll project4) ,添加如下代码:
// 导出函数,使用“ _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) ”(在工程目录下的Debug
2. 用DllImport调用DLL中的count函数
1) ”,向“Form1窗体中添加一个按钮。
2) 为 ”,Text“用DllImport中count调整到适当大小,移到适当位置。
3) .cs和 extern ”,并使其具有来自 Count.dll 的实现,代码如下:
[DllImport("Count.dll")]
static extern int count(int init);
4) .cs,在“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) 复制到项目“Tzb文件夹中,按“F5运行该程序,并点击按钮B2
个提示框显示的是调用“count(0)个提示框显示的是调用“count(10)调用DLL动态调用DLL
3. C#动态调用DLL中的函数
中使用DllImport那样,所以只能借助API中,与动态库调用有关的函数包括[3]①LoadLibrary的AfxLoadLibrary②GetProcAddress内部地址。
(或MFC),释放动态链接库。
现在,我们可以用IntPtr hModule=LoadLibrary(“Count.dll”);的句柄,”);中是没有函数指针的,没有像C++及System.Reflection.Assembly1) dld ”,打开类视图,右击“Tzb“类”,类名设置为“dld的每个单词的开头字母。
2.
using System.Runtime.InteropServices; // 用 DllImport 需用此 命名空间
using System.Reflection; // 使用 Assembly 类需用此 命名空间
using System.Reflection.Emit; // 使用 ILGenerator 需用此 命名空间
”上面添加如下代码声明参数传递方式枚举:
/// <summary>
/// 参数传递方式枚举 ,ByValue 表示值传递 ,ByRef 表示址传递
/// </summary>
public enum ModePass
{
ByValue = 0x0001,
ByRef = 0x0002
}
3. 、GetProcAddress及私有变量hModule:
/// <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. 方法,并为了调用时方便,重载了这个方法:
/// <summary>
/// 装载 Dll
/// </summary>
/// <param name="lpFileName">DLL 文件名 </param>
public void LoadDll(string lpFileName)
{
hModule=LoadLibrary(lpFileName);
if(hModule==IntPtr.Zero)
throw(new Exception(" 没有找到 :"+lpFileName+"." ));
}
的句柄,可以使用LoadDll
public void LoadDll(IntPtr HMODULE)
{
if(HMODULE==IntPtr.Zero)
throw(new Exception(" 所传入的函数库模块的句柄 HMODULE 为空 ." ));
hModule=HMODULE;
}
5. 方法,并为了调用时方便,也重载了这个方法,方法的具体代码及注释如下:
/// <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)
{
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 ”,向“Form1窗体中添加和Text“B3、“用LoadLibrary”,“B4、“调用count””,并调整到适当的大小及位置。
2. .cs,在“B3_Click类实例:
/// <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. .cs,在“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. .cs,在“B5_Click
myfun.UnLoadDll();
6. ”运行该程序,并先点击按钮B3加载“Count.dll”三次以调用3”,先后弹出的提示框如下:
经初始化后,再传入实参“0也不会改变其值为“0。
7. 以卸载“Count.dll进行装载“Count.dll查看调用了“count(0)
(三) 调用托管DLL一般方法
C# 是很简单的,只要在“解决方案资源管理器”中的需要调用DLL或通过浏览来选择DLL导入相关的命名空间。
(四) 动态调用托管DLL
C# 也需要借助System.Reflection.Assembly。现在,用例子说明:
,新建一个Visual C# ”,并在类“Class1中及方法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
}
”,向“Form1窗体中添加属性为“B6,Text类来动态调用托管DLL,转入代码视图,先导入命名空间:using System.Reflection; 方法和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
”方法体内代码如下:
// 显示 count(0) 返回的值
MessageBox.Show(" 这是您第 "+Invoke("CsCount.dll","CsCount","Class1","count",new object[]{(int)0}).ToString()+" 次点击此按钮。 "," 挑战杯 ");
”的bin\Debug复制到项目“Tzb文件夹中,按“F5运行该程序,并点击按钮B6个提示框,内容分别是“这是您第 1次点击此按钮。”、“这是您第 3在这里的作用。
(五) C#程序嵌入DLL的调用
DLL程序byte[]”返回,然后就用“Assembly Load(byte[]);”得到DLL程序集,最后就可以像上面的Invoke中的方法进行调用。当然不用上面方法也可以,如用接口实现动态调用,但DLL。现在我只对像上面的Invoke中的方法进行调用进行讨论,为了以后使用方便及实现代码的复用,我们可以结合上一个编写一个类。
1) ldfs”中新建一个名为ldfs”,请注意,在这个类中“resource程序中的资源,它也可以是硬盘上任意一个DLL的类中的方法LoadDll,如果找不到,就查找硬盘上的。
using // 对文件的读写需要用到此命名空间
using System.Reflection; // 使用 Assembly 类需用此命名空间
using System.Reflection.Emit; // 使用 ILGenerator 需用此命名空间
:
// 记录要导入的程序集
static Assembly MyAssembly;
方法:
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
}
方法来卸载DLL
public void UnLoadDll()
{// 使 MyAssembly 指空
MyAssembly=null;
}
方法来进行对DLL.cs相同,不过这里用的是Assembly.Load”,而且用了来保存已加载的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) ldfs1. 作为“嵌入的资源”添加到项目“Tzb2. ”窗体中添加两个按钮,Name属性分别为“B7调用count”、“UnLoadDll3. .cs实例:
// 添加一个 ldfs 实例 tmp
private ldfs tmp=new ldfs();
4. .cs,在“B1_Click
// 调用 count(0), 并使用期提示框显示其返回值
MessageBox.Show(" 这是您第 "+tmp.Invoke("CsCount.dll","CsCount","Class1","count",new object[]{(int)0}).ToString()+" 次点击此按钮。 "," 挑战杯 ");
5. .cs,在“B1_Click
// 卸载 DLL
tmp.UnLoadDll();
6. ”运行该程序,并先点击按钮B7,最后再点击按钮B7类的使用”类似,。
、附件2:dld.cs、附件4:Count.cpp。
三、 结 论
有很多优点,如:节省内存和减少交换操作;开发大型程序时可以把某些模块分配给程序员,程序员可以用任何一门他所熟悉的语言把该模块编译成DLL也有一些不足,如在提要中提及的问题。所以,如何灵活地调用DLL语言有很多优点,越来越多的人开始使用它来编程。但是,C#或C++;另外,一般认为C#反编译而得到部分源码,所以需要使用混合编程加强C#嵌入C#文件先用某一算法进行加密甚至压缩后再作为资源文件添加到C#
转自断点社区 http://bbs.