您的位置:首页 > 编程语言 > C#

C#程序实现动态调用DLL的研究

2007-08-20 16:50 841 查看
[align=center]C#程序实现动态调用DLL的研究[/align]
摘 要:在《csdn开发高手》2004年第03期中的《化功大法——将DLL嵌入EXE》一文,介绍了如何把一个动态链接库作为一个资源嵌入到可执行文件,在可执行文件运行时,自动从资源中释放出来,通过静态加载延迟实现DLL函数的动态加载,程序退出后实现临时文件的自动删除,从而为解决“DLL Hell”提供了一种解决方案。这是一个很好的设计思想,而且该作者也用C++实现了,在Internet上也有相似的VB程序,但在某一技术论坛上提起这种设计方法时,有网友提出:“这种方法好是好,但就是启动速度太慢”。这是因为程序启动时实现DLL释放,然后再加载释放出来的DLL,这个过程会耗费一定的时间。鉴于此问题,经过思索,提出另一个设计方案:DLL作为资源文件嵌入程序,但不需进行DLL释放及其重新加载。本文就是对该设计方案的原理分析及使用C#编程来实现该设计方案。

关键词:动态调用DLL,嵌入DLL,C#

正 文:
一、 推广模块式体系结构
DLL 有助于促进模块式程序的开发。这可以帮助您开发要求提供多个语言版本的大型程序或要求具有模块式体系结构的程序。模块式程序的一个示例是具有多个可以在运行时动态加载的模块的计帐程序。
3) 调用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. 改变按钮的属性:Name为 “B1”,Text为 “用DllImport调用DLL弹出提示框”,并将按钮B1调整到适当大小,移到适当位置。
4. 按“F5”运行该程序,并点击按钮B1,便弹出如下提示框:



[align=center] [/align]
(二) 启动Visual C++ 6.0;
2) 打开Count.cpp,添加如下代码:
[align=center]// 导出函数,使用“ _stdcall ” 标准调用 [/align]
[align=center]extern "C" _declspec(dllexport)int _stdcall count(int init); [/align]
int _stdcall count(int init)
{//count 函数,使用参数 init 初始化静态的整形变量 S ,并使 S 自加 1 后返回该值
static int S=init;
S++;
return S;
}
5) 打开项目“Tzb”,向“Form1”窗体中添加一个按钮。
2) 在“Form1.cs[设计]”视图中双击按钮B2,在“B2_Click”方法体内添加如下代码:
[align=left]MessageBox.Show(" 用 DllImport 调用 DLL 中的 count 函数, \n 传入的实参为 0 ,得到的结果是: "+count(0).ToString()," 挑战杯 "); [/align]
[align=left]MessageBox.Show(" 用 DllImport 调用 DLL 中的 count 函数, \n 传入的实参为 10 ,得到的结果是: "+count(10).ToString()+"\n 结果可不是想要的 11 哦!!! "," 挑战杯 "); [/align]
MessageBox.Show(" 所得结果表明: \n 用 DllImport 调用 DLL 中的非托管 \n 函数是全局的、静态的函数!!! "," 挑战杯 ");
[align=center] [/align]
5)”);来获得函数的入口地址。
但是,知道函数的入口地址后,怎样调用这个函数呢?因为在C#中是没有函数指针的,没有像C++那样的函数指针调用方式来调用函数,所以我们得借助其它方法。经过研究,发现我们可以通过结合使用System.Reflection.Emit及System.Reflection.Assembly里的类和函数达到我们的目的。为了以后使用方便及实现代码的复用,我们可以编写一个类。
[b]1) 添加所需的命名空间及声明参数传递方式枚举:
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. 添加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. 打开项目“Tzb”,向“Form1”窗体中添加三个按钮。Name 和Text属性分别为 “B3”、“用LoadLibrary方法装载Count.dll”,“B4”、“调用count方法”,“B5”、“卸载Count.dll”,并调整到适当的大小及位置。
2.
4. 按“F5”运行该程序,并先点击按钮B3以加载“Count.dll”,接着点击按钮B4三次以调用3次“count(0)”,先后弹出的提示框如下:







这三个提示框所得出的结果说明了静态变量S 经初始化后,再传入实参“0”也不会改变其值为“0”。
7.
动态调用托管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
[/b]ldfs类的使用:
1. 打开“Form1.cs”代码视图,添加一个ldfs实例:
// 添加一个 ldfs 实例 tmp
private ldfs tmp=new ldfs();
4. “F5”运行该程序,并先点击按钮B7三次,接着点击按钮B8,最后再点击按钮B7,此时发现又开始重新计数了,情况和“dld类的使用”类似,也就是也实现了DLL的动态装载与卸载了。
说明:以上所用到的所有源代码详见附件1:Form1.cs、附件2:dld.cs、附件3:ldfs.cs、附件4:Count.cpp、附件5:Class1.cs。
[align=center] [/align]
[b]三、
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  职场 C# DLL