在前一篇Blog,动态平台调用 part 1 中,介绍了一种进行动态平台调用的方法。
除了这种方法之外,还能利用.NET提供的强大的反射(Reflection)功能,来达到动态平台调用的目的。System.Reflection.Emit命名空间下的类和方法提供了强大的运行时创建和执行代码的能力,利用这些类和方法进行动态平台调用,主要步骤如下:
1. 添加如下两个命名空间:
using System.Reflection.Emit;
using System.Reflection;
2. 使用DefineDynamicAssembly创建一个动态程序集(assembly)对象;
3. 使用DefineDynamicModule为第二步创建的程序集定义一个动态模块(module);
4. 使用DefinePInvokeMethod为第三步创建的模块定义一个用于访问非托管函数的P/Invoke方法;
5. 调用CreateGlobalFunctions函数完成动态模块的全局函数定义和全局数据定义;
6. 调用GetMethod函数获得之前定义的用于进行平台调用的方法;
7. 调用上一步获得的方法完成对非托管函数的平台调用。
下面的代码演示了如何采用上述方法对非托管函数进行动态平台调用。
class DynamicPInvokeViaEmit
{
public static void Test()
{
string currentDirectory =
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string dllPath = Path.Combine(currentDirectory,
@"nativelibfordynamicpinvoke\NativeLibForDynamicPInvoke.dll");
//定义参数和返回值的类型
Type[] parameterTypes = new Type[] { typeof(int), typeof(int) };
int factorA = 100, factorB = 8;
Type returnType = typeof(int);
//设置参数的值
object[] parameterValues = { factorA, factorB };
// 设置函数入口点,必须是经过名称重整后的函数名
// 原因在于非托管DLL使用的调用约定是__stdcall
// 如果使用Multiply,将会抛出异常
string entryPoint = "_Multiply@8";
//设置DllImport的字段
CallingConvention nativeCallConv = CallingConvention.StdCall;
CharSet nativeCharSet = CharSet.Ansi;
//设置方法的修饰符,至少需要设置static和extern
MethodAttributes methodAttr =
MethodAttributes.Static
| MethodAttributes.Public
| MethodAttributes.PinvokeImpl;
//进行动态平台调用
object result = DynamicDllFunctionInvoke(
dllPath,
entryPoint,
methodAttr,
nativeCallConv,
nativeCharSet,
returnType,
parameterTypes,
parameterValues
);
//打印结果
Console.WriteLine(
string.Format("{0} * {1} = {2} ",
factorA,
factorB,
result)
);
Console.WriteLine("Press any key to exit.");
Console.Read();
}
public static object DynamicDllFunctionInvoke(
string dllPath,
string entryPoint,
MethodAttributes methodAttr,
CallingConvention nativeCallConv,
CharSet nativeCharSet,
Type returnType,
Type[] parameterTypes,
object[] parameterValues
)
{
string dllName = Path.GetFileNameWithoutExtension(dllPath);
// 创建一个动态组件(assembly)和模块(module)
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = string.Format("A{0}{1}",
dllName,
Guid.NewGuid().ToString("N")
);
AssemblyBuilder dynamicAssembly =
AppDomain.CurrentDomain.DefineDynamicAssembly(
assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder dynamicModule =
dynamicAssembly.DefineDynamicModule(
string.Format("M{0}{1}",
dllName,
Guid.NewGuid().ToString("N"))
);
// 使用指定的信息创建平台调用签名
MethodBuilder dynamicMethod =
dynamicModule.DefinePInvokeMethod(
entryPoint,
dllPath,
methodAttr,
CallingConventions.Standard,
returnType,
parameterTypes,
nativeCallConv,
nativeCharSet
);
// 创建函数
dynamicModule.CreateGlobalFunctions();
// 获得平台调用的方法
MethodInfo methodInfo =
dynamicModule.GetMethod(entryPoint, parameterTypes);
// 调用非托管函数并获得返回的结果
object result = methodInfo.Invoke(null, parameterValues);
return result;
}
}
在上面的DynamicPInvokeViaEmit类中,定义了用于对非托管方法进行动态平台调用的函数DynamicDllFunctionInvoke。只要传递定义非托管方法的DLL名称、函数入口点、该方法的属性、该方法的调用约定、该方法的返回类型、该方法的参数类型以及平台调用标志,就能完成对该非托管方法的动态平台调用。
在DynamicPInvokeViaEmit类的Test方法中,首先对非托管函数的参数和返回值的类型进行了定义,并设置了参数的值。接着设置了平台调用所需的必要信息,比如函数入口点、调用约定,字符集等。这些信息都是为了对非托管函数进行平台调用所需进行的必要准备工作。最后,将这些信息传递给DynamicDllFunctionInvoke方法,就能对非托管函数进行动态平台调用了。
需要特别注意的是函数入口点的名称。由于调用约定能够影响非托管函数被导出的实际名称。由于本示例所采用的非托管DLL在导出函数时,使用了extern "C",并采用了__stdcall这个调用约定,因此最终导出的函数名就会被重整成_Multiply@8。这样就要求在使用DefinePInvokeMethod方法定义用于访问非托管函数的平台调用方法时,必须传递同非托管DLL导出函数时的最终函数名完全相同的名称。因此在指定函数入口点时,就必须采用经过重整后的名称,也就是_Multiply@8。如果使用了Multiply,就会抛出EntryPointNotFoundException异常。这一点同直接采用DllImport属性来声明非托管函数时,无需自己手动指定最终的函数名是完全不同的。这是由于在托管代码中声明非托管函数时,CLR会根据DllImport属性的CallingConvention字段的值,采用相应调用约定所对应的名称重整规则,自动完成函数名的重整工作。并使用经过名称重整后的实际函数名,在非托管DLL中查找对应的非托管函数。
运行Test方法进行测试,同样也能获得完全相同的结果:
100 * 8 = 800 Press any key to exit. |