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

1 .Net互操作
.Net不能直接操作非托管代码,这时就需要互操作了。
1.1 P/Invoke
许多常用Windows操作都有托管接口,但是还有许多完整的 Win32 部分没有托管接口。如何操作呢?平台调用 (P/Invoke) 就是完成这一任务的最常用方法。要使用 P/Invoke,您可以编写一个描述如何调用函数的原型,然后运行时将使用此信息进行调用。

1.1.1 枚举和常量
以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(BeepType beepType);   
现在我可以用下面的语句来调用它: MessageBeep(BeepType.IconQuestion);
如果常量为其他类型(非int),则需要修改枚举类型的基本类型
enum Name : Type {…}
1.1.2 处理普通结构体
有时我需要确定我笔记本的电池状况。Win32 为此提供了电源管理函数。
BOOL GetSystemPowerStatus(
 LPSYSTEM_POWER_STATUS lpSystemPowerStatus
);   
此函数包含指向某个结构的指针,我们尚未对此进行过处理。要处理结构,我们需要用 C# 定义结构。我们从非托管的定义开始:
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# 版本。
struct SystemPowerStatus
{
  byte ACLineStatus;
  byte batteryFlag;
  byte batteryLifePercent;
  byte reserved1;
  int batteryLifeTime;
  int batteryFullLifeTime;
}
这样,就可以方便地编写出 C# 原型:
[DllImport("kernel32.dll")]
public static extern bool GetSystemPowerStatus( ref SystemPowerStatus systemPowerStatus);   
在此原型中,我们用“ref”指明将传递结构指针而不是结构值。这是处理通过指针传递的结构的一般方法。
此函数运行良好,但是最好将 ACLineStatus 和 batteryFlag 字段定义为 enum:
enum ACLineStatus: byte
{
 Offline = 0,
 Online = 1,
 Unknown = 255,
}
enum BatteryFlag: byte
{ ...}   
请注意,由于结构的字段是一些字节,因此我们使用 byte 作为该 enum 的基本类型。

1.1.3 处理内嵌指针的结构体
有时我们要调用的函数的参数为包含指针的结构体,对于这样的参数,如何处理呢?
struct CXTest
{
LPBYTE pData;     // 一个指向byte数组的指针
int nLen;         // 数组的长度
}
BOOL WINAPI XFunction(const CXTest &inData_, CXTest &outData_);
在C#中我们如何去调用呢
struct CXTest
{
public IntPrt pData;
public int nLen;
}
static extern bool XFunction(ref [In] CXTest inData_, ref CXTest 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数据;还有最后要记得释放申请的内存。
 
1.1.4 处理内嵌数组与字符串的结构体
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 void Init()
{
 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,不能为其复制。由于结构体不能自定义缺省参数,所以增加一个Init函数或通过类来替换来初始化byData。
从底层接口中获取数据一定要使用struct,且从底层接口中(out)获取数据后,byData就自动指向了实际的内容了。向底层接口中设定数据时,如果使用struct一定要先调用init,并且通过ref方式;如果是类,则不能使用ref修饰(C#中:类默认放在堆中,结构体默认放在栈中的)。
 
1.1.5 字符串与字符串缓冲区
在 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 传递的是缓冲区大小。

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

封送不透明 (Opaque) 指针:一种特殊情况
有时在 Windows API 中,方法传递或返回的指针是不透明的,这意味着该指针值从技术角度讲是一个指针,但代码却不直接使用它。相反,代码将该指针返回给 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 静态公共字段。

1.1.7 回调函数
当 Win32 函数需要返回多项数据时,通常都是通过回调机制来实现的。开发人员将函数指针传递给函数,然后针对每一项调用开发人员的函数。
在 C# 中没有函数指针,而是使用“委托”,在调用 Win32 函数时使用委托来代替函数指针。EnumDesktops() 函数就是这类函数的一个示例:
BOOL EnumDesktops(
 HWINSTA hwinsta, // 窗口实例的句柄
 DESKTOPENUMPROC lpEnumFunc, // 回调函数
 LPARAM lParam// 用于回调函数的值
);   
HWINSTA 类型由 IntPtr 代替,而 LPARAM 由 int 代替。DESKTOPENUMPROC 所需的工作要多一些。下面是 MSDN 中的定义:
BOOL CALLBACK EnumDesktopProc(
 LPTSTR lpszDesktop, // 桌面名称
 LPARAM lParam// 用户定义的值
);   
我们可以将它转换为以下委托:
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() 这样的函数调用后,底层保存以便以后使用,则托管代码需要保证在使用委托时,委托引用还是有效的(没有把回收掉),此中情况下,一般要设为全局。

1.1.8 属性的其他选项
DLLImport 和 StructLayout 属性具有一些非常有用的选项,有助于 P/Invoke 的使用。另外返回值可以Return属性进行修饰。
DLL Import 属性
除了指出宿主 DLL 外,DllImportAttribute 还包含了一些可选属性,其中四个特别有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。
    EntryPoint:在不希望外部托管方法具有与 DLL 导出相同的名称的情况下,可以设置该属性来指示导出的 DLL 函数的入口点名称。当您定义两个调用相同非托管函数的外部方法时,这特别有用。
    CharSet:如果 DLL 函数不以任何方式处理文本,则可以忽略 DllImportAttribute 的 CharSet 属性。然而,当 Char 或 String 数据是等式的一部分时,应该将 CharSet 属性设置为 CharSet.Auto。这样可以使 CLR 根据宿主 OS 使用适当的字符集。如果没有显式地设置 CharSet 属性,则其默认值为 CharSet.Ansi。
    SetLastError:设为true后,会导致 CLR 在每次调用外部方法之后缓存由 API 函数设置的错误。然后,在包装方法中,可以通过调用System.Runtime.InteropServices.Marshal.GetLastWin32Error 方法来获取缓存的错误值。然后检查这些期望来自 API 函数的错误值,并为这些值引发一个可感知的异常。对于其他所有失败情况(包括根本就没意料到的失败情况),则引发在 System.ComponentModel.Win32Exception异常,并将 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_)

1.1.9 其他问题
从不同位置加载
您无法指定希望 DLLImport 在运行时从何处查找文件,但是可以利用一个技巧来达到这一目的。
DllImport 调用 LoadLibrary() 来完成它的工作。如果进程中已经加载了特定的 DLL,那么即使指定的加载路径不同,LoadLibrary() 也会成功。
这意味着如果直接调用 LoadLibrary(),您就可以从任何位置加载 DLL,然后 DllImport LoadLibrary() 将使用该 DLL。
由于这种行为,我们可以提前调用 LoadLibrary(),从而将您的调用指向其他 DLL。如果您在编写库,可以通过调用 GetModuleHandle() 来防止出现这种情况,以确保在首次调用 P/Invoke 之前没有加载该库。

P/Invoke 疑难解答
如果您的 P/Invoke 调用失败,通常是因为某些类型的定义不正确。以下是几个常见问题:
    long != long。在 C++ 中,long 是 4 字节的整数,但在 C# 中,它是 8 字节的整数。
    字符串类型设置不正确。

对于非常复杂的结构,通过P/Invoke还是很难处理的,这是可考虑使用C++ Inerop来处理。

1.2 C++ Interop
使用P/Invoke可以封送大部分的操作,但是对于复杂的操作处理起来就非常麻烦,同时无法处理异常(无法获取原来异常的真实信息)。同时,一般来说Interop性能比较好。
1.2.1 托管类型
C++下的类、结构体、枚举等,不能在托管C++下直接使用,需要使用托管的类、结构体与枚举类型:ref class、ref struct与enum class。
C++下的指针与引用也不能在托管C++下,需要分别替换为跟踪句柄(^)与跟踪引用(%)。同样,数组与字符串也需要替换为:String^与array^。
托管C++下的常量需要使用literal来修饰。
String^ strVerb=nullptr;    //不能直接使用NULL
array^ strNames={“Jill”, “Tes”};
array^ 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:
    BOOL bValid;
DWORD nCount;
LARGE_INTEGER liNumber;
WCHAR wzName[10];
BYTE byBuff[100];
CPPSubStruct stSub;
}
#pragma pack(pop, MyPack_H)
对应的.Net定义
[StructLayout(LayoutKind::Sequential, Pack = 4, CharSet = CharSet::Unicode)]
ref struct MyStruct
{
public:
    MyStruct()
    {
        // 必须先使用gcnew为数组与结构体分配空间,字符串不需要
        byBuff = gcnew array(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^ byBuff;
    [MarshalAs(UnmanagedType::Struct)]
    MySubStruct    ^ stSub;
};


1.2.2 字符串与数组转换
可通过中的pin_ptr把托管字符串与数组转换为非托管的字符串与数组:
pin_ptr pKeySN = PtrToStringChars(strKeySN_)
wchar_t     wzUser[CLen::CKeySNLen+1];
GetNameBySN(pKeySN, wzUser);
return gcnew String(wzUser);
转换字符串时,需要用到PtrToStringChars来获取指针;如果是数组,直接使用第一个元素的地址即可(&Elments[0]),但是如果如果数组指针为空需要先判断,设_xPtr为托管数组指针(如array^ byBuffer):
 ( ((nullptr == _xPtr) || (0 == _xPtr->Length)) ? nullptr : &_xPtr[0] )
数组操作:
int GetInfo(IntPtr hHandle, [Out] array^ %byInfo)
{    
int nLen = 100;
array^ byKey = gcnew array(100);
pin_ptr pBuff = &byKey[0];
int nCount = CPPGetInfo(hHandle.ToPointer(),pBuff, nLen);

byInfo = gcnew array(nLen);
Array::Copy(byKey, byInfo, nLen);
return nCount;
}
为了能回传byMySubStruct,必须使用跟踪引用(%)。
托管内存使用gcnew来申请(不需要手动释放),然后使用pin_ptr转换为非托管的指针(当然,此处也完全可以使用pBuffer[100]来代替),通过Copy把非托管内容复制到托管数字钟;通过ToPointer()来获取非托管指针。

1.2.3 回调函数
声明
[UnmanagedFunctionPointer(CallingConvention::StdCall)]
delegate int CallbackFun(…);
设定(设CPPCallbackFun为CallbackFun的C++对应声明)
void SetCallback(CallbackFun^ delFun_)
{
IntPtr ptrCallback = Marshal::GetFunctionPointerForDelegate(delFun_);
CPPSetCallback(static_cast(ptrCallback.ToPointer()));
}

1.2.4 异常处理
非托管的异常无法在托管程序中使用,必须先捕获非托管的异常,然后再转换为托管的异常。
设CPPException为C++下的异常,DotNetException(需要继承标准异常,如ApplicationException、Exception等)为托管异常
try
{
……
}
catch(CPPException &ex)
{
    throw gcnew DotNetException(gcnew String(ex.GetMsg()), ex.GetCode());
}
捕获C++异常时,需要使用引用,防止出现截断现象;新抛出的托管异常需要gcnew出来。


你可能感兴趣的:(Dot,Net)