C#调用非托管DLL函数

一 概念

运行时处于控制之下的代码称为托管代码,反之,在运行环境(runtime)之外运行的代码称为非托管代码。

平台调用(来自consuming unmanaged dll functions)是一个可以使托管代码调用在DLL中实现的非托管函数,例如那些win32 API。它找到和调用导出的函数,同时根据需要将函数的参数(整形,字符,数组,结构体等等)通过互操作边界(interoperation boundary)进行转换(marshal)。

平台调用依赖于元数据来定位导出函数,并在运行时进行参数封组装好(来自a closer look at platform invoke)。调用过程如下

C#调用非托管DLL函数_第1张图片

<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);
}


2创建一个容纳DLL函数的类

将一个经常使用的DLL函数打包到一个类里面是一个封装平台功能的有效方法。虽然并不是在每个情况下都强制要求这样,但是提供一个类来包装是有它的方便性,因为定义DLL函数是繁琐并且容易出错的。如果使用C#或VB编程,应该将DLL函数声明在类或VB模块中。

在一个类中,为每个想要调用的DLL函数定义一个静态方法。这个定义可以包含额外的信息,例如字符集,或者在传参过程的调用惯例(calling convention)。可以通过使用默认设置来忽略这些信息。

一旦完成打包,可以像调用其他的静态函数一样调用这些函数,系统调用会自动处理底层的导出函数,就像前面示例代码中的样子。


你可能感兴趣的:(windows,C#)