在托管(Managed)代码中调用原生(Native)Dll的手段和调试方法

所谓托管(Managed)代码通常指.Net Framework里面的代码,例如VB.Net、C#代码,原生(Native)代码指的是用原先的C/C++开发的代码。大部分开源代码往往是原生(Native)代码,因为这样的代码可以在多种平台上(Windows/Unix/Linux/MacOs)编译运行,而托管(Managed)代码,由于目前.Net Framework不具有多平台的兼容性,只能在Windows上运行。

从托管(Managed)代码调用原生(Native)代码开发的DLL的概念叫做平台调用(Platform Invoke),如下图所示。参见.Net Framework 高级开发 - A Closer Look at Platform Invoke

调用原生(Native)代码

如果有原生(Native)代码的源程序,那不仅可以调用它,还可以进行调试(Debug)。如果没有源程序,只有可执行部分,例如DLL,就只能调用,无法调试。

要调用原生(Native)代码,只要把代码编译成为DLL,放置在调用程序相同的目录即可。例如,用VB.Net代码编译出来的可执行程序是hello.exe,位于目录bin下面,用Ansi C代码编译出来的DLL是world.dll,也把它放到目录bin下面,这样hello.exe就可以调用world.dll中的函数了。

如果有原生(Native)代码的源程序,可以在vs2k5(Visual Studio 2005)建立一个混合模式的解决方案(Solution),为托管(Managed)代码和原生(Native)代码分别建立项目(Project)。在vs2k5中,不同语法的代码是不能放在同一个项目(Project)中编译的。

例如,在解决方案(Solution)中,先加入一个叫做hello的VB.Net项目(Project),项目类型是Window Application,即目标程序是exe程序;然后再加入一个叫做world的C++的项目(Project),项目类型是Win32 Project,选择DLL模式。

虽然是C++项目,也是一样可以编译C语言程序的。如果是C语言程序,那在C++项目中的属性(Property)中,要选择“Compile as C Code”模式,具体位置如下。

Configuration Properties -> C/C++ -> Advanced -> Compile As

缺省情况下面,C++项目的目标目录和托管(Managed)代码的目标目录是不同的。这样,托管(Managed)代码在调用DLL时候,会出现找不到DLL的错误。可以用下面两种方法解决这个问题。

一种方法是,修改C++项目的目标目录设置,和托管(Managed)代码的目标目录相同。这种方法有个小缺陷,如果解决方案(Solution)中有多个可执行程序,这样设置只能解决其中一个程序调用DLL的问题。设置位置如下。

项目属性(Property) -> Configuration Properties -> General -> Output Diretory

另外一种方法是,在C++项目的Post-Build Event中,增加拷贝命令,将目标DLL复制到相应的目标目录。这种方法可以把目标DLL复制到任意个目标目录中。vs2k5是很智能的,在程序调试的时候,发现将要载入的DLL和某个项目的目标DLL相同时,就会载入这个项目的目标DLL和调试信息,进行调式。设置位置如下。

项目属性(Property) -> Configuration Properties -> Build Events -> Post-Build Event -> Command Line

一个命令行的例子如下。

copy $(TargetPath) 目标目录1
copy $(TargetPath) 目标目录2
copy $(TargetPath) 目标目录3

托管(Managed)代码项目和原生(Native)DLL项目的这种调用关系,形成了一种项目依赖(Dependencies)。也就是说,原生(Native)DLL项目应该在调用项目之前编译。这种依赖是vs2k5无法感知的,需要手工设置,把每个调用原生(Native)DLL项目都设置成依赖DLL项目的形式,这样就可以形成正确的编译顺序(Build Order)。设置位置如下。

菜单 -> Project -> Project Dependencies -> Dependencies

在VB.Net中调用原生(Native)DLL

在VB.Net中可以与Declare语句和DllImport属性两种方式来调用原生(Native)DLL中的函数。

Declare语句是比较常用的方法,从VB的早期版本开始就有这个语句。一个典型的Declare语句的例子如下。

Declare Auto Function MBox Lib "user32.dll" Alias "MessageBox" ( _
ByVal hWnd As Integer, _
ByVal txt As String, _
ByVal caption As String, _
ByVal Typ As Integer _
) As Integer

上面的例子中,Lib关键词指定了DLL的名字和位置(可执行程序的当前目录),Alias关键词指定了执行函数的名字,Auto关键词指定了String类型参数的转换规则。Declare语句隐含说明了这个函数是Shared类型的。详细解释参见VB参考手册:Declare Statement

DllImport是VB.Net中才引入的方法,一个典型的DllImport语句的例子如下。

Imports System.Runtime.InteropServices
...
<DllImport ("user32.dll", EntryPoint:="MessageBox")> _
Public Shared Function MessageBox (
ByVal hWnd As Integer, _
ByVal txt As String, ByVal caption As String, _
ByVal Typ As Integer _
) As IntPtr
End Function

上面的例子中,第1个参数是dllName,指定了DLL的名字和位置(可执行程序的当前目录),EntryPoint是第5个参数,指定了执行函数的名字。如果要向前面Declare语句那样指定String类型参数的转换规则,可以使用CharSet参数。注意,这样的函数(Function)或者子程序(Sub)必须是Shared类型的,而且应该是空函数或者空子程序。

DllImport的参数比较多,所以和Declare语句相比,可以更加详细的指定调用原生(Native)DLL的细节。DllImport的参数依次为dllName、BestFitMapping、CallingConvention、CharSet、EntryPoint、ExactSpelling、PreserveSig、SetLastError和ThrowOnUnmappableChar。详细解释参见.Net类库参考手册:DllImportAttribute Members

从形式上说,DllImport属性和Declare语句的功能是大致相同的,不过使用DllImport属性有一个优点,vs2k5能够在编译的时候检查参数的类型和原生(Native)DLL中的函数参数是不是相匹配,而使用Declare语句则没有这种检查,检查只有在执行到相应函数的时候发生。

参数封送(Marshal)

调用原生(Native)DLL最主要的麻烦是参数封送(Marshal),就是VB.Net或者托管(Managed)代码中的参数如何与原生(Native)DLL中的函数交互。下表列出了一些常用的类型对应关系,此表来自Visual Studio编程说明:Platform Invoke Data Types

非托管类型(Wtypes.h)非托管类型(C语言)托管类型描述
HANDLEvoid*System.IntPtr32位或64位
BYTEunsigned charSystem.Byte8位
SHORTshortSystem.Int1616位
WORDunsigned shortSystem.UInt1616位
INTintSystem.Int3232位
UINTunsigned intSystem.UInt3232位
LONGlongSystem.Int3232位
BOOLlongSystem.Int3232位
DWORDunsigned longSystem.UInt3232位
ULONGunsigned longSystem.UInt3232位
CHARcharSystem.CharANSI
LPSTRchar*System.String 或 System.Text.StringBuilderANSI
LPCSTRConst char*System.String 或 System.Text.StringBuilderANSI
LPWSTRwchar_t*System.String 或 System.Text.StringBuilderUnicode
LPCWSTRConst wchar_t*System.String 或 System.Text.StringBuilderUnicode
FLOATFloatSystem.Single32位
DOUBLEDoubleSystem.Double64位

参数类型封送(Marshal)是相当复杂的,不同的类型有不同的对应方法。对于字符串(String)、类(Class)、结构(Structure)、联合(Union)、数组(Array)、函数回调(Callback)、void指针(void *)都有各种不同的对应方法。在Visual Studio编程说明:Marshaling Data with Platform Invoke中有多节说明,以及多个示例解释。

当然还有一种简单的方法来解决参数封送(Marshal),就是到互联网上去找一下别人写的封送(Marshal)代码,比如用相应的函数名到Google Group里面去找找,往往能找到。

为DLL函数建立一个专门的类

调用原生(Native)DLL并不是很常见的事情,如果程序能够调用托管(Managed)类库解决问题,就不要调用原生(Native)DLL。对于DLL函数的调用说明最好建立在一个专门的类中,进行封装。参见.Net Framework 高级开发 - Creating a Class to Hold DLL Functions

调试原生(Native)DLL

要调试原生(Native)DLL,大致需要下面这几个条件和步骤。

  • 要有原生(Native)DLL的源代码。
  • 为原生(Native)DLL的源代码建立一个单独的项目(Project)。
  • 将这个项目加入到含调用这个DLL的托管(Managed)代码项目的解决方案中(Solution)。
  • 通过设置DLL项目的目标目录,或者在DLL项目的Post-Build Event设置拷贝命令,让托管(Managed)代码项目能够载入相应的DLL。这一条在前文中已经有详细说明。

除了这些以外,还有一些设置需要注意。

首先,原生(Native)DLL的项目编译选项中在连接器(Linker)部分要设定产生调试信息,否则肯定不能调试,无法设定断点,无法进行代码跟踪,最多只能进行汇编级别的调试。一般来说,项目都有Debug和Release两个配置(Configuration),在Debug配置中,要设定产生调试信息,Release就不用了。设置位置如下,要设置为Yes。

项目属性(Property) -> Configuration Properties -> Linker -> Debugging -> Generate Debug Info

其次,托管(Managed)代码项目要开启混合调试模式(Debug in Mixed Mode)。例如,对于VB.Net项目,设定位置如下,要在设定前打勾。参见Visual Studio 应用程序开发 - How to: Debug in Mixed Mode

项目属性(Property) -> Debug -> (Enable Debuggers) Enable unmanaged code debugging

参考资料

作者: 杰棍 [Jegwon]

波波坡原创文章 链接:http://www.bobopo.com/article/code/call_native_dll.htm

标签:

关键词: 托管代码, Managed Code, 原生DLL, Native DLL, 调用, C++, Ansi C, VB.Net, DotNet, 参数封送, Marshal, DllImport

创建日期: 2008-01-20

文库 微博 博客 作品 首页