P/Invoke应用备忘
P/Invoke -- Platform Invoke:提供了一种从托管代码访问并调用非托管代码的方法,应用场景包括从托管代码直接调用Win32 API或其他一些非托管代码实现的库等。
在最近一个蓝牙通信相关的小项目中,需要用到第三方提供的由C语言编写的底层通信API,并希望用C#和Winform快速完成界面开发,P/Invoke正好可以满足需要。
使用P/Invoke调用非托管代码的前提和主要工作就是确保托管/非托管代码之间正确的映射,包括:
1. 为使用的每个方法提供正确的声明;
2. 完成方法参数、返回值的正确映射,包括基本类型、结构体、指针(函数指针)等;
方法声明
P/Invoke要求方法须声明为static和extern的,static是因为非托管代码可能没有"对象实例"的概念,extern则是要让编译器知道这是一个DLL导入的方法,不需要在这里提供方法实现。在方法声明处,还要附上"DllImport"属性(来自System.Runtime.InteropServices.DllImportAttribute),该属性标明所引用的DLL名称,如下例:
原版声明:long MyNaiveFunc(); //位于GoodAPIs.dll
托管声明:[DllImport("GoodAPIs.dll")]
static extern int MyNaiveFunc();
这样声明之后,托管代码就可以像使用其他方法一样使用这个API了。
DllImport属性还支持一些参数,以常用的"EntryPoint"和"SetLastError"为例:
[DllImport("GoodAPIs.dll", EntryPoint="MyNaiveFunc", SetLastError=true)]
static extern int MyFancyFunc();
默认情况下,声明的方法名称须和DLL中的目标方法同名,以便P/Invoke能找到正确的方法地址,但使用"EntryPoint"参数指定目标方法名称后,我们就可以自定义对应的托管名称了;"SetLastError"在调用Win32 API时尤其有用,它通知CLR在每次执行该调用时保存API设置的错误值,随后,我们可以通过Marshal类(同样来自System.Runtime.InteropServices)的GetLastWin32Error()方法获得可能的错误信息。(GetLastWin32Error()正是对kernal32.dll中的GetLastError()的封装)
类型映射
1 基本类型映射
非托管代码中的类型和.NET中支持的类型大致有如下的映射关系:
| C/C++ | C# |
HANDLE, LPDWORD, LPVOID, void* |
IntPtr |
LPCTSTR, LPCTSTR, LPSTR, char*, const char*, Wchar_t*, LPWSTR |
String [in], StringBuilder [in, out] |
DWORD, unsigned long, Ulong |
UInt32, [MarshalAs(UnmanagedType.U4)] |
bool |
bool |
LP<struct> |
[In] ref <struct> |
SIZE_T |
uint |
LPDWORD |
out uint |
LPTSTR |
[Out] StringBuilder |
PULARGE_INTEGER |
out ulong |
WORD |
Uint16 |
Byte, unsigned char |
byte |
Short |
Int16 |
Long, int |
Int32 |
float |
single |
double |
double |
NULL pointer |
IntPtr.Zero |
Uint |
Uint32 |
如第一个例子中,C方法返回值的类型是long,而在托管代码中,则对应int(即System.Int32)。
2 结构体
C/C++代码中存在大量结构体定义,要在托管代码中使用它们,也需要进行声明,例如:
原版声明:typedef struct MyNaiveStru{
unsigned char uc;
unsigned long ul;
char chArr[32];
}
托管声明:[StructLayout(LayoutKind.Sequential)]
public struct MyFancyStru{
public byte uc;
public int ul;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public char[] chArr;
}
有几点需要说明:首先,用class和struct声明都可以;第二,声明时要附加StructLayout属性,将结构体的内存布局设置为"顺序的",这是因为如果不设置,CLR就可能为优化内存使用而打乱结构体中成员的位置,这将导致与非托管代码交互时的混乱;第三,MarshalAs属性的作用是让我们自定义类型的映射关系,在这里为了正确将c#定义的数组映射为中的char[32],我们将它映射为UnmanagedType.ByValArray,这时,需用SizeConst参数设置数组大小。
3 指针
在上面的映射表中,已经给出了一些指针的映射关系,实际上在很多情况下,可以直接使用C#的ref/out来处理指针的情况,举例说明(略去DllImport):
原版声明:void MyNaiveFunc(unsigned long *pul);
托管声明:static extern MyNaiveFunc(ref uint pul);
对于在方法中赋值并传出的字符串,可如下例处理:
原版声明:int GetStrByPara(char *str);
托管声明:int GetStrByPara([Out, MarshalAs(UnmanagedType.LPStr)] StringBuilder str);
对于结构体指针,可如下处理:
原版声明:void WorkWithStru(MyStru *pStru);
托管声明:void WorkWithStru(ref MyStru pStru);
或: void WorkWithStru(IntPtr pStru); //这个方法是开始时用的,现在看来其实没有必要,还是记录一下;
其中pStru定义为:
MyStru stru = new MyStru();
IntPtr pStru = Marshal.AllocHGlobal(Marshal.SizeOf(MyStru));
Marshal.StructureToPtr(stru, pStru, false);
//use it...
Marshal.FreeHGlobal(pStru);
当声明为结构体指针的参数实际想要传出的是结构体数组时,甚至可以这样:
原版声明:void GetStruArray(MyStru *pStrus);
托管声明:void GetStruArray(MyStru[] pStrus);
另一种常用的类型是函数指针,我们可以直接定义相同(映射)签名的delegate来进行对应:
原版声明:typedef void (MyCallbackFunc)(unsgined int hdl, unsigned char *param);
void WorkWithCBK(MyCallbackFunc *cbk);
托管声明:public delegate void MyCallbackFunc(uint hdl, IntPtr param); //param在我实际的case中虽然声明为unsigned char *,但它其实可以转化为指向多种类型的指针,所以这里用IntPtr,在callback内部,根据情况可使用Marshal.PtrToStructure()进行转换..
void WorkWithCBK(MyCallbackFunc cbk);
浙公网安备 33010602011771号