运行时处于控制之下的代码称为托管代码,反之,在运行环境(runtime)之外运行的代码称为非托管代码。
平台调用(来自consuming unmanaged dll functions)是一个可以使托管代码调用在DLL中实现的非托管函数,例如那些win32 API。它找到和调用导出的函数,同时根据需要将函数的参数(整形,字符,数组,结构体等等)通过互操作边界(interoperation boundary)进行转换(marshal)。
平台调用依赖于元数据来定位导出函数,并在运行时进行参数封组装好(来自a closer look at platform invoke)。调用过程如下
<1> 定位到包含对应函数的DLL
<2> 将DLL加载进入内存
<3> 定位函数在内存中的数据,并将它的参数传递到栈上,根据需要进行数据封组
<4> 将控制权转交给非托管函数
先看一个示例程序
在vs创建一个C++的win32的空dll项目dllTest,新建一个名为dllTest.cpp文件,添加如下代码
extern "C" __declspec(dllexport) int Add(int x, int y)
{
return x + y;
}
extern "C" __declspec(dllexport) int Sub(int x, int y)
{
return x - y;
}
extern "C" __declspec(dllexport) int Multiply(int x, int y)
{
return x * y;
}
extern "C" __declspec(dllexport) int Divide(int x, int y)
{
return x / y;
}
编译后生成dllTest.dll文件
现在要在C#代码中调用该dll的函数,新建一个C#的控制台工程,将program.cs的内容修改如下
class Program
{
static void Main(string[] args)
{
int result = CPPDLL.Add(10, 20);
Console.WriteLine("10 + 20 = {0}", result);
result = CPPDLL.Sub(30, 12);
Console.WriteLine("30 - 12 = {0}", result);
result = CPPDLL.Multiply(5, 4);
Console.WriteLine("5 * 4 = {0}", result);
result = CPPDLL.Divide(30, 5);
Console.WriteLine("30 / 5 = {0}", result);
Console.ReadLine();
}
}
class CPPDLL
{
[DllImport(@"E:\kfq\vc_projects\dllTest\Debug\dllTest.dll")]
public static extern int Add(int x, int y);
[DllImport(@"E:\kfq\vc_projects\dllTest\Debug\dllTest.dll")]
public static extern int Sub(int x, int y);
[DllImport(@"E:\kfq\vc_projects\dllTest\Debug\dllTest.dll")]
public static extern int Multiply(int x, int y);
[DllImport(@"E:\kfq\vc_projects\dllTest\Debug\dllTest.dll")]
public static extern int Divide(int x, int y);
}
记得添加命名空间System.Runtime.InteropServices。编译运行,但是需要注意的是编译平台要设置成x86,不然运行时会出错,提示“试图加载格式不正确的程序”。
1确定DLL中的函数
确定dll函数包含下面几点
函数名或序号(ordinal,也许是可以通过dll中导出函数的序号获取函数)
dll的文件名
例如,在user32.dll中指定MessageBox函数和它的位置。微软的窗口程序编程接口(Win32 API)都包含两个版本用于处理characters和strings:1字节长度的ANSI版本和2字节长度的Unicode版本。当没有指定的时候,由ChartSet代表的字符集,默认为ANSI。有些函数可以有多于两个的版本。
MessageBoxA是MessageBox函数的ANSI版本入口,MessageBoxW则是Unicode版本。可以使用一系列的命令行工具输出指定dll的导出函数。例如,可以使用dumpbin /exports user32.dll 或link /dump /exports user32.dll 获取函数名称。
可以将非托管函数重命名为任意想要的名称,只要将新的名称映射到DLL中原先的函数名。可以通过指定入口点来重命名非托管函数。
一个入口点(参见Specifying an Entry Point)指明了DLL中一个函数的位置。在托管工程中,目标函数的初始名称或者序号入口点通过互操作边界来鉴别函数。更进一步的,可以将入口点映射到不同的名称,从而有效的重命名这个函数。
重命名一个DLL函数的可能原因有这些:
避免区分大小写的API函数名
为了和当前的命名规则一致
为了包含使用不同数据类型的函数(通过定义同一个DLL函数的多个版本)
简化使用包含ANSI和Unicode版本的API
可以通过名称或序号,由DllImportAttribute.EntryPoint字段来指定DLL的函数。如果在外部定义的函数名称与DLL中的入口名称一致,就可以不用显式的指定EntryPoint字段,否则就应该使用如下的形式:
[DllImport("dllname", EntryPoint="Functionname")]
[DllImport("dllname", EntryPoint="#123")]
注意序号前必须加一个”#”前缀
下面是一个使用MsgBox来替代DLL中的MessageBoxA的例子
using System.Runtime.InteropServices;
public class Win32
{
[DllImport("user32.dll",EntryPoint="MessageBoxA")]
publicstaticexternint MsgBox(int hWnd,String text,String caption,uint type);
}
将一个经常使用的DLL函数打包到一个类里面是一个封装平台功能的有效方法。虽然并不是在每个情况下都强制要求这样,但是提供一个类来包装是有它的方便性,因为定义DLL函数是繁琐并且容易出错的。如果使用C#或VB编程,应该将DLL函数声明在类或VB模块中。
在一个类中,为每个想要调用的DLL函数定义一个静态方法。这个定义可以包含额外的信息,例如字符集,或者在传参过程的调用惯例(calling convention)。可以通过使用默认设置来忽略这些信息。
一旦完成打包,可以像调用其他的静态函数一样调用这些函数,系统调用会自动处理底层的导出函数,就像前面示例代码中的样子。