在托管(Managed)代码中调用原生(Native)Dll的手段和调试方法(转)
所谓托管(Managed)代码通常指.NetFramework里面的代码,例如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下面,用AnsiC代码编译出来的DLL是world.dll,也把它放到目录bin下面,这样hello.exe就可以调用world.dll中的函数了。
如果有原生(Native)代码的源程序,可以在vs2k5(Visual Studio2005)建立一个混合模式的解决方案(Solution),为托管(Managed)代码和原生(Native)代码分别建立项目(Project)。在vs2k5中,不同语法的代码是不能放在同一个项目(Project)中编译的。
例如,在解决方案(Solution)中,先加入一个叫做hello的VB.Net项目(Project),项目类型是WindowApplication,即目标程序是exe程序;然后再加入一个叫做world的C++的项目(Project),项目类型是Win32Project,选择DLL模式。
虽然是C++项目,也是一样可以编译C语言程序的。如果是C语言程序,那在C++项目中的属性(Property)中,要选择“Compile as C Code”模式,具体位置如下。
Configuration Pr operties -> C/C+ + -> Adv anced -> Compile As
缺省情况下面,C++项目的目标目录和托管(Managed)代码的目标目录是不同的。这样,托管(Managed)代码在调用DLL时候,会出现找不到DLL的错误。可以用下面两种方法解决这个问题。
一种方法是,修改C++项目的目标目录设置,和托管(Managed)代码的目标目录相同。这种方法有个小缺陷,如果解决方案(Solution)中有多个可执行程序,这样设置只能解决其中一个程序调用DLL的问题。设置位置如下。
项目属性(Property) -> Con figurati on Prope rties -> General -> Outp ut Diret ory
另外一种方法是,在C++项目的Post-BuildEvent中,增加拷贝命令,将目标DLL复制到相应的目标目录。这种方法可以把目标DLL复制到任意个目标目录中。vs2k5是很智能的,在程序调试的时候,发现将要载入的DLL和某个项目的目标DLL相同时,就会载入这个项目的目标DLL和调试信息,进行调式。设置位置如下。
项目属性(Property) -> Con figurati on Prope rties -> Build E vents -> Post-Bu ild Even t -> Com mand Lin e
一个命令行的例子如下。
copy $(TargetPat h) 目标目录1
copy $(TargetPat h) 目标目录2
copy $(TargetPat h) 目标目录3
托管(Managed)代码项目和原生(Native)DLL项目的这种调用关系,形成了一种项目依赖(Dependencies)。也就是说,原生(Native)DLL项目应该在调用项目之前编译。这种依赖是vs2k5无法感知的,需要手工设置,把每个调用原生(Native)DLL项目都设置成依赖DLL项目的形式,这样就可以形成正确的编译顺序(Build Order)。设置位置如下。
菜单 -> Project -> P roject D ependenc ies -> D ependenc ies
在VB.Net中调用原生(Native)DLL
在VB.Net中可以与Declare语句和DllImport属性两种方式来调用原生(Native)DLL中的函数。
Declare语句是比较常用的方法,从VB的早期版本开始就有这个语句。一个典型的Declare语句的例子如下。
DeclareAuto Fun ction MB ox Lib " user32.d ll" Alia s "Messa geBox" ( _
ByVal hWnd As In teger, _
ByVal txt As Str ing, _
ByVal caption As String, _
ByVal Typ As Int eger _
) As Integer
上面的例子中,Lib关键词指定了DLL的名字和位置(可执行程序的当前目录),Alias关键词指定了执行函数的名字,Auto关键词指定了String类型参数的转换规则。Declare语句隐含说明了这个函数是Shared类型的。详细解释参见VB参考手册:Declare Statement。
DllImport是VB.Net中才引入的方法,一个典型的DllImport语句的例子如下。
ImportsSystem.R untime.I nteropSe rvices
...
<DllImport ("u ser32.dl l", Entr yPoint:= "Message Box")> _
Public Shared Fu nction M essageBo x (
ByVal hWnd As In teger, _
ByVal txt As Str ing, ByV al capti on As St ring, _
ByVal Typ As Int eger _
) 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语言) | 托管类型 | 描述 |
---|---|---|---|
HANDLE | void* | System.IntPtr | 32位或64位 |
BYTE | unsigned char | System.Byte | 8位 |
SHORT | short | System.Int16 | 16位 |
WORD | unsigned short | System.UInt16 | 16位 |
INT | int | System.Int32 | 32位 |
UINT | unsigned int | System.UInt32 | 32位 |
LONG | long | System.Int32 | 32位 |
BOOL | long | System.Int32 | 32位 |
DWORD | unsigned long | System.UInt32 | 32位 |
ULONG | unsigned long | System.UInt32 | 32位 |
CHAR | char | System.Char | ANSI |
LPSTR | char* | System.String 或 System.Text.StringBuilder | ANSI |
LPCSTR | Const char* | System.String 或 System.Text.StringBuilder | ANSI |
LPWSTR | wchar_t* | System.String 或 System.Text.StringBuilder | Unicode |
LPCWSTR | Const wchar_t* | System.String 或 System.Text.StringBuilder | Unicode |
FLOAT | Float | System.Single | 32位 |
DOUBLE | Double | System.Double | 64位 |
参数类型封送(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) -> Con figurati on Prope rties -> Linker -> Debug ging -> Generate Debug I nfo
其次,托管(Managed)代码项目要开启混合调试模式(Debug in Mixed Mode)。例如,对于VB.Net项目,设定位置如下,要在设定前打勾。参见Visual Studio 应用程序开发 - How to: Debug in Mixed Mode。
项目属性(Property) -> Deb ug -> (E nable De buggers) Enable unmanage d code d ebugging