.Net调用非托管代码(P/Invoke与C++InterOP)
Posted 飞鹰技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了.Net调用非托管代码(P/Invoke与C++InterOP)相关的知识,希望对你有一定的参考价值。
.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 传递的是缓冲区大小。
指针参数
封送不透明 (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);
( ((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出来。
以上是关于.Net调用非托管代码(P/Invoke与C++InterOP)的主要内容,如果未能解决你的问题,请参考以下文章
[.NET] 平台调用(P/Invoke) 与 DllImport 使用的相关讲解与注意事项,
[.NET] 平台调用(P/Invoke) 与 DllImport 使用的相关讲解与注意事项,