在托管(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 Properties -> C/C++ -> Advanced -> Compile As

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

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

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

另外一种方法是,在C++项目的Post-BuildEvent中,增加拷贝命令,将目标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语言) 托管类型 描述
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) -> 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

参考资料

你可能感兴趣的:(native)