华泰证券投资理财联盟

.Net调用非托管代码(P/Invoke与C++InterOP)

飞鹰技术 2019-08-12 11:04:30

.Net中默认不能直接操作调用非托管代码,但很多时候托管代码又是必须的,那如何调用呢?



平台调用P/Invoke

许多常用Windows操作都有托管接口,但是还有许多 Win32 API是没有托管接口的,如何操作呢?平台调用 (P/Invoke) 就是完成这一任务的最常用方法。要使用 P/Invoke,就需要编写一个描述如何调用的函数原型,然后通过此原型调用对应接口。


枚举和常量

以MessageBeep()为例。MSDN 给出了以下原型:

BOOL MessageBeep(

 UINT uType // 声音类型

); 

这看起来很简单,但是从注释中可以发现两个有趣的事实。

  • uType 参数实际上接受一组预先定义的常量。

  • 可能的参数值包括 -1,这意味着尽管它被定义为uint 类型,但 int 会更加适合。对于 uType 参数,使用 enum 类型是合乎情理的。

public enum BeepType

{

  SimpleBeep = -1,

  IconAsterisk =0x00000040,

  IconExclamation =0x00000030,

  IconHand = 0x00000010,

  IconQuestion =0x00000020,

  Ok = 0x00000000,

}

[DllImport("user32.dll")]

public static extern bool MessageBeep(BeepTypebeepType);  


现在可以用下面的语句来调用它: MessageBeep(BeepType.IconQuestion);

若常量为非int类型,则需要修改枚举类型的基本类型

enum Name : Type {…}


处理普通结构体

以Win32中的电源管理函数为例。

BOOL GetSystemPowerStatus(

 LPSYSTEM_POWER_STATUSlpSystemPowerStatus

);  

此函数包含指向某个结构的指针,其结构体为:

typedef struct _SYSTEM_POWER_STATUS {

  BYTE  ACLineStatus;

  BYTE  BatteryFlag;

  BYTE  BatteryLifePercent;

  BYTE  Reserved1;

  DWORD BatteryLifeTime;

  DWORD BatteryFullLifeTime;

} SYSTEM_POWER_STATUS, *LPSYSTEM_POWER_STATUS;

我们需要用 C# 定义一个对应的C#版本(用 C# 类型代替 C 类型):

struct SystemPowerStatus

{

  byte ACLineStatus;

  byte batteryFlag;

  byte batteryLifePercent;

  byte reserved1;

  int batteryLifeTime;

  int batteryFullLifeTime;

}

这样,就可以方便地编写出C# 原型:

[DllImport("kernel32.dll")]

public static extern bool GetSystemPowerStatus( refSystemPowerStatus systemPowerStatus);  


在此原型中,我们用“ref”指明将传递结构指针而不是结构值。这是处理通过指针传递的结构的一般方法。

此函数运行良好,但是最好将ACLineStatus 和 batteryFlag 字段定义为 enum:

enum ACLineStatus: byte

{

 Offline = 0,

 Online = 1,

 Unknown = 255,

}

enum BatteryFlag: byte

{ ...}  

请注意,由于结构的字段是一些字节,因此我们使用 byte 作为该 enum 的基本类型。


处理内嵌指针的结构体

有时我们要调用函数的参数为包含指针的结构体,对于这样的参数,如何处理呢?

struct CXTest

{

LPBYTE pData;  // 一个指向byte数组的指针

int nLen;      // 数组的长度

}

BOOL WINAPI XFunction(const CXTest &inData_, CXTest&outData_);

在C#中我们使用IntPtr替换C中的指针

struct CXTest

{

public IntPtr pData;

public int nLen;

}

static extern bool XFunction(ref [In] CXTest inData_, refCXTest outData_);

下面就来看一下具体调用了,设数组长度为nDataLen

CXTest stIn = new CXTest(), stOut = new CXTest();

byte[] pIn = new byte[nDataLen];

// 为数组赋值

stIn.pData = Marshal.AllocHGlobal(nDataLen);

Marshal.Copy(pIn, 0, stIn.pData, nDataLen);

stIn.nLen = nDataLen;

stOut.pData = Marshal.AllocHGlobal(nDataLen);

stOut.nLen = nDataLen;

XFunction(ref stIn, ref stOut);

byte[] pOut = new byte[nDataLen];

Marshal.Copy(stOut.pData, pOut, 0, nDataLen);

// ....

Marshal.FreeHGlobal(stIn.pData);

Marshal.FreeHGlobal(stOut.pData);


此处最重要的是要注意,pData的内存要先申请,再向里copy数据;还有最后要记得释放申请的内存。

 

处理内嵌数组与字符串的结构体

C/C++下的定义与实现:

struct CXTest 

{

WCHAR wzName[64];

int nLen;

byte byData[100];

}

bool SetTest(const CXTest &stTest_);

在C#下,为了方便初始化byte数组,我们使用类来代替结构

[StructLayout(LayoutKind.Sequential, Pack=2,CharSet=CharSet.Unicode)]

class CXTest

{

public CXTest()

{

strName= "";

nLen =0;

byData =new byte[100];

}

[MarshalAs(UnmanagedType.ByValTStr,SizeConst = 64))]

public string strName;

public int nLen;

[MarshalAs(UnmanagedType.ByValArray,SizeConst = 100)]

public byte[] byData;

}

stataic extern bool SetTest(CXTest stTest_);


虽然为byData预留的空间,但是其指向null,必须在使用前先初始化byData

若是结构体,必须使用ref修饰;如果是类,则不能使用ref修饰(C#中:类默认放在堆中,结构体默认放在栈中的)。

 

字符串与字符串缓冲区

在 Win32 中有两种不同的字符串表示:ANSI、Unicode。 P/Invoke 提供了内置的支持来自动使用 A 或 W 版本(若调用的函数不存在,互操作层将尝试查找并使用 A 或 W 版本)。但是互操作的默认字符类型是 Ansi 或单字节,如果非托管代码为宽字符,则需要明确的把CharSet设为CharSet.Unicode。

.NET 中的字符串类型是不可改变的类型,这意味着它的值将永远保持不变。对于要将字符串值复制到字符串缓冲区的函数,字符串将无效(这样做至少会破坏由封送拆收器在转换字符串时创建的临时缓冲区;严重时会破坏托管堆)。

因此字符串缓冲区要使用StringBuilder 类型来代替字符串。

C格式函数声明:

DWORD GetShortPathName(

  LPCTSTR lpszLongPath,

  LPTSTR lpszShortPath,

  DWORD cchBuffer

);

C#中封装

[DllImport("kernel32.dll", CharSet =CharSet.Auto)]

public static extern int GetShortPathName(

 [MarshalAs(UnmanagedType.LPTStr)]

  string path,

 [MarshalAs(UnmanagedType.LPTStr)]

  StringBuilder shortPath,

  int shortPathLength);  

使用此函数很简单:

StringBuilder shortPath = new StringBuilder(80);

int result = GetShortPathName(@"d:\dest.jpg", shortPath, shortPath.Capacity);

string s = shortPath.ToString();

请注意,StringBuilder的 Capacity 传递的是缓冲区大小。


指针参数

许多 Windows API函数将指针作为它们的一个或多个参数。指针增加了封送数据的复杂性,因为它们增加了一个间接层。如果没有指针,您可以通过值在线程堆栈中传递数据。有了指针,则可以通过引用传递数据,方法是将该数据的内存地址推入线程堆栈中。然后,函数通过内存地址间接访问数据。使用托管代码表示此附加间接层的方式有多种。

 

封送不透明 (Opaque) 指针:一种特殊情况

有时在 WindowsAPI 中,方法传递或返回的指针是不透明的,这意味着该指针值从技术角度讲是一个指针,但代码却不直接使用它。相反,代码将该指针返回给 Windows 以便随后进行重用。一个非常常见的例子就是句柄的概念。

当一个不透明指针返回给您的应用程序(或者您的应用程序期望得到一个不透明指针)时,您应该将参数或返回值封送为 CLR 中的一种特殊类型 —System.IntPtr。当您使用 IntPtr 类型时,通常不使用 out 或 ref 参数,因为 IntPtr 意为直接持有指针。不过,如果您将一个指针封送为一个指针,则对 IntPtr 使用 by-ref 参数是合适的。

在 CLR 类型系统中,System.IntPtr 类型有一个特殊的属性。不像系统中的其他基类型,IntPtr 并没有固定的大小。相反,它在运行时的大小是依底层操作系统的正常指针大小而定的。这意味着在32 位的 Windows 中,IntPtr 变量的宽度是 32 位的,而在 64 位的 Windows 中,实时编译器编译的代码会将 IntPtr 值看作 64 位的值。当在托管代码和非托管代码之间封送不透明指针时,这种自动调节大小的特点十分有用。

您可以在托管代码中将IntPtr 值强制转换为 32 位或 64 位的整数值,或将后者强制转换为前者。然而,当使用 Windows API 函数时,因为指针应是不透明的,所以除了存储和传递给外部方法外,不能将它们另做它用。这种“只限存储和传递”规则的两个特例是当您需要向外部方法传递 null 指针值和需要比较 IntPtr 值与 null 值的情况。为了做到这一点,您不能将零强制转换为System.IntPtr,而应该在 IntPtr 类型上使用 Int32.Zero 静态公共字段。


回调函数

当 Win32 函数需要返回多项数据时,通常都是通过回调机制来实现的。开发人员将函数指针传递给函数,然后针对每一项调用开发人员的函数。

在 C# 中没有函数指针,而是使用“委托”,在调用 Win32 函数时使用委托来代替函数指针。EnumDesktops() 函数就是这类函数的一个示例:

BOOL EnumDesktops(

 HWINSTA hwinsta, // 窗口实例的句柄

 DESKTOPENUMPROC lpEnumFunc, // 回调函数

 LPARAM lParam// 用于回调函数的值

);  

HWINSTA 类型由 IntPtr 代替,而 LPARAM 由 int 代替。DESKTOPENUMPROC所需的工作要多一些。下面是MSDN 中的定义:

BOOL EnumDesktopProc(

 LPTSTR lpszDesktop, // 桌面名称

 LPARAM lParam// 用户定义的值

);  

我们可以将它转换为以下委托:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)] // 一定要加,而且根据实际情况设定调用约定

delegate bool EnumDesktopProc(

 [MarshalAs(UnmanagedType.LPTStr)]

 string desktopName,

 int lParam);  

完成该定义后,我们可以为EnumDesktops() 编写以下定义:

[DllImport("user32.dll", CharSet = CharSet.Auto)]

static extern bool EnumDesktops(

  IntPtr windowStation,

  EnumDesktopProc callback,

  int lParam);  

这样该函数就可以正常运行了。

 

在互操作中使用委托时有个很重要的技巧:封送拆收器创建了指向委托的函数指针,该函数指针被传递给非托管函数。但是,封送拆收器无法确定非托管函数要使用函数指针做些什么,因此它假定函数指针只需在调用该函数时有效即可。因此,如果委托是通过诸如SetCallback() 这样的函数调用后,底层保存以便以后使用,则托管代码需要保证在使用委托时,委托引用还是有效的(没有被回收掉),此中情况下,一般要设为全局。


属性的其他选项

DLLImport 和 StructLayout 属性具有一些非常有用的选项,有助于 P/Invoke 的使用。另外返回值可以Return属性进行修饰。

  • DLLImport 属性:除了指出宿主 DLL 外,DllImportAttribute 还包含了一些可选属性,其中四个特别有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。

    • EntryPoint:在不希望外部托管方法具有与 DLL 导出相同的名称的情况下,可以设置该属性来指示导出的 DLL 函数的入口点名称。

    • CharSet:如果 DLL 函数不以任何方式处理文本,则可以忽略DllImportAttribute 的 CharSet 属性;若参数中涉及到字符与字符串,则需要根据实际情况小心设定。如果没有显式地设置 CharSet 属性,则其默认值为 CharSet.Ansi。

    • SetLastError:设为true后,会导致 CLR 在每次调用外部方法之后缓存由 API 函数设置的错误(通过调用System.Runtime.InteropServices.Marshal.GetLastWin32Error方法来获取缓存的错误值)。

    • CallingConvention :通过此属性,可以给 CLR 指示应该将哪种函数调用约定用于堆栈中的参数。CallingConvention.Winapi的默认值是最好的选择,它在大多数情况下都可行。然而,如果该调用不起作用,则可以检查 Platform SDK 中的声明头文件,看看您调用的 API 函数是否是一个不符合调用约定标准的异常 API。

  • StructLayout属性

    • LayoutKind:结构在默认情况下按顺序布局,并且在多数情况下都适用。如果需要完全控制结构成员所放置的位置,可以使用 LayoutKind.Explicit,然后为每个结构成员添加 FieldOffset 属性。当您需要创建 union 时,通常需要这样做。

    • CharSet:控制 ByValTStr 成员的默认字符类型。

    • Pack:设置结构的压缩大小,它控制结构的排列方式。如果 C 结构采用了其他压缩方式,则需要设置此属性。

    • Size:设置结构大小。不常用;但是如果需要在结构末尾分配额外的空间,则可能会用到此属性。

  • 返回值:可修改返回的类型,一般都是bool类型需要处理。

[DllImport("user32.dll", SetLastError = true)]

[return: MarshalAs(UnmanagedType.Bool)]

private static extern bool GetLastInputInfo(ref XLastInputInfo stInfo_)

 

其他问题

  • 从不同位置加载

    • 您无法指定希望DLLImport 在运行时从何处查找文件,但是可以利用一个技巧来达到这一目的。

    • DllImport 调用 LoadLibrary() 来完成它的工作。如果进程中已经加载了特定的 DLL,那么即使指定的加载路径不同,LoadLibrary() 也会成功。

    • 这意味着如果直接调用LoadLibrary(),您就可以从任何位置加载DLL,然后 DllImpor的tLoadLibrary() 将使用该 DLL。

    • 由于这种行为,我们可以提前调用 LoadLibrary(),从而将您的调用指向其他 DLL。如果您在编写库,可以通过调用 GetModuleHandle() 来防止出现这种情况,以确保在首次调用 P/Invoke 之前没有加载该库。

  • P/Invoke疑难解答:如果您的 P/Invoke 调用失败,通常是因为某些类型的定义不正确。以下是几个常见问题:

    • long != long。在 C++ 中,long 是 4 字节的整数,但在 C# 中,它是 8 字节的整数。

    • 字符串类型设置不正确。

  


C++Interop

使用P/Invoke可以封送大部分的操作,但是对于复杂的操作处理起来就非常麻烦,同时无法处理异常(无法获取原来异常的真实信息)。同时,一般来说Interop性能比较好。


托管类型

  • C++下的类、结构体、枚举等,不能在托管C++下直接使用,需要使用托管的类、结构体与枚举类型:ref class、ref struct与enum class。

  • C++下的指针与引用也不能在托管C++下,需要分别替换为跟踪句柄(^)与跟踪引用(%)。

  • 数组与字符串也需要替换为:String^与array<type>^。

  • 托管C++下的常量需要使用literal来修饰。

String^ strVerb=nullptr;   //不能直接使用NULL

array<String^>^ strNames={“Jill”, “Tes”};

array<int>^ nWeight = {130, 168};

int nValue = 10;

int% nTrackValue=nValue;

literal int NameMaxlen = 64;

 

定义结构体时,需要使用StructLayout与Marshal属性进行修改,以如下C++结构体为例:

#pragma pack(push, MyPack_H, 4)

struct CPPStruct

{

public:

    BOOLbValid;

    DWORDnCount;

    LARGE_INTEGERliNumber;

    WCHARwzName[10];

    BYTEbyBuff[100];

    CPPSubStructstSub;

}

#pragma pack(pop, MyPack_H)

对应的.Net定义

[StructLayout(LayoutKind::Sequential, Pack = 4,CharSet = CharSet::Unicode)]

ref struct MyStruct

{

public:

      MyStruct()

      {

             // 必须先使用gcnew为数组与结构体分配空间,字符串不需要

             byBuff =gcnew array<unsigned char>(100)

             stSub =gcnew MySubStruct();

      }

     [MarshalAs(UnmanagedType::Bool)]

      bool bValid;

      int nCount;

      long long llNumber;

      [MarshalAs(UnmanagedType::ByValTStr,SizeConst = 10)]

      String^ strName;

      [MarshalAs(UnmanagedType::ByValArray,SizeConst = 100)]

      array<unsignedchar>^ byBuff;

      [MarshalAs(UnmanagedType::Struct)]

      MySubStruct ^ stSub;

};

  

字符串与数组转换

可通过<vcclr.h>中的pin_ptr把托管字符串与数组转换为非托管的字符串与数组:

pin_ptr<const wchar_t> pKeySN =PtrToStringChars(strKeySN_)

wchar_t      wzUser[CLen::CKeySNLen+1];

GetNameBySN(pKeySN, wzUser);

return gcnew String(wzUser);

转换字符串时,需要用到PtrToStringChars来获取指针;如果是数组,直接使用第一个元素的地址即可(&Elments[0]),但是如果数组指针为空需要先判断,设_xPtr为托管数组指针(如array<unsigned char>^byBuffer):

 ( ((nullptr ==_xPtr) || (0 == _xPtr->Length)) ? nullptr : &_xPtr[0] )

数组操作:

int GetInfo(IntPtr hHandle, [Out] array<unsignedchar>^ %byInfo)

{    

int nLen= 100;

array<unsignedchar>^ byKey = gcnew array<unsigned char>(100);

pin_ptr<unsignedchar> pBuff = &byKey[0];

int nCount= CPPGetInfo(hHandle.ToPointer(),pBuff, nLen);

 

byInfo =gcnew array<unsigned char>(nLen);

Array::Copy(byKey,byInfo, nLen);

return nCount;

}

为了能回传byInfo,必须使用跟踪引用(%)。

托管内存使用gcnew来申请(不需要手动释放),然后使用pin_ptr转换为非托管的指针(当然,此处也完全可以使用pBuffer[100]来代替),通过Copy把非托管内容复制到托管数字钟;通过ToPointer()来获取非托管指针。

 

回调函数

声明

[UnmanagedFunctionPointer(CallingConvention::StdCall)]

delegate int CallbackFun(…);

设定(设CPPCallbackFun为CallbackFun的C++对应声明)

void SetCallback(CallbackFun^ delFun_)

{

IntPtrptrCallback = Marshal::GetFunctionPointerForDelegate(delFun_);

CPPSetCallback(static_cast<CPPCallbackFun>(ptrCallback.ToPointer()));

}

 

异常处理

非托管的异常无法在托管程序中使用,必须先捕获非托管的异常,然后再转换为托管的异常。

设CPPException为C++下的异常,DotNetException(需要继承标准异常,如ApplicationException、Exception等)为托管异常

try

{

    ……

}

catch(CPPException &ex)

{

      throw gcnew DotNetException(gcnewString(ex.GetMsg()), ex.GetCode());

}

捕获C++异常时,需要使用引用,防止出现截断现象;新抛出的托管异常需要gcnew出来。


Copyright © 华泰证券投资理财联盟@2017