C#程序实现动态调用DLL (2)

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”上面添加如下代码声明参数传递方式枚举:

///
/// 参数传递方式枚举 ,ByValue 表示值传递 ,ByRef 表示址传递
///
public enum ModePass
{
ByValue = 0x0001,
ByRef = 0x0002
}

3.       声明LoadLibrary、GetProcAddress、FreeLibrary及私有变量hModule和farProc:

///
/// 原型是 :HMODULE LoadLibrary(LPCTSTR lpFileName);
///
/// DLL 文件名
/// 函数库模块的句柄
[DllImport("kernel32.dll")]
static extern IntPtr LoadLibrary(string lpFileName);
///
/// 原型是 : FARPROC GetProcAddress(HMODULE hModule, LPCWSTR lpProcName);
///
/// 包含需调用函数的函数库模块的句柄
/// 调用函数的名称
/// 函数指针
[DllImport("kernel32.dll")]
static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
///
/// 原型是 : BOOL FreeLibrary(HMODULE hModule);
///
/// 需释放的函数库模块的句柄
/// 是否已释放指定的 Dll
[DllImport("kernel32",EntryPoint="FreeLibrary",SetLastError=true)]
static extern bool FreeLibrary(IntPtr hModule);
///
/// Loadlibrary 返回的函数库模块的句柄
///
private IntPtr hModule=IntPtr.Zero;
///
/// GetProcAddress 返回的函数指针
///
private IntPtr farProc=IntPtr.Zero;

4.       添加LoadDll方法,并为了调用时方便,重载了这个方法:

///
/// 装载 Dll
///
/// DLL 文件名
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方法,并为了调用时方便,也重载了这个方法,方法的具体代码及注释如下:

///
/// 获得函数指针
///
/// 调用函数的名称
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+" 这个函数的入口点 "));
}
///
/// 获得函数指针
///
/// 包含需调用函数的 DLL 文件名
/// 调用函数的名称
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方法也进行了重载:

///
/// 卸载 Dll
///
public void UnLoadDll()
{
FreeLibrary(hModule);
hModule=IntPtr.Zero;
farProc=IntPtr.Zero;
}

            Invoke方法的第一个版本:

///
/// 调用所设定的函数
///
/// 实参
/// 实参类型
/// 实参传送方式
/// 返回类型
/// 返回所调用函数的 object
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方法的第二个版本,它是调用了第一个版本的:

///
/// 调用所设定的函数
///
/// 函数指针
/// 实参
/// 实参类型
/// 实参传送方式
/// 返回类型
/// 返回所调用函数的 object
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类实例:

///
/// 创建一个 dld 类对象
///
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#)