参见Marshaling Data with Platform Invoke
要调用从非托管库导出的函数,一个.net框架程序需要一个在托管代码中定义好该函数的函数原型。为了创建一个能让系统调用正确进行数据封组的原型,必须做到如下几点:
应用DllImportAttribute到托管代码中的静态函数或方法
使用托管数据类型替换非托管数据类型
下面的表格列出了用于Win32 API和C风格函数的数据类型。多数非托管库都包含以这些数据类型作为参数或者返回值的函数。第三列列出了对应.Net框架内置的数据类型,用于托管代码。在一些场合中,可以使用列表中相同数据长度的另一类型去替换。
Unmanaged type in Wtypes.h |
Unmanaged C language type |
Managed class name |
Description |
HANDLE |
void* |
System.IntPtr |
32 bits on 32-bit Windows operating systems, 64 bits on 64-bit Windows operating systems. |
BYTE |
unsigned char |
System.Byte |
8 bits |
SHORT |
short |
System.Int16 |
16 bits |
WORD |
unsigned short |
System.UInt16 |
16 bits |
INT |
int |
System.Int32 |
32 bits |
UINT |
unsigned int |
System.UInt32 |
32 bits |
LONG |
long |
System.Int32 |
32 bits |
BOOL |
long |
System.Int32 |
32 bits |
DWORD |
unsigned long |
System.UInt32 |
32 bits |
ULONG |
unsigned long |
System.UInt32 |
32 bits |
CHAR |
char |
System.Char |
Decorate with ANSI. |
WCHAR |
wchar_t |
System.Char |
Decorate with Unicode. |
LPSTR |
char* |
System.String or System.Text.StringBuilder |
Decorate with ANSI. |
LPCSTR |
Const char* |
System.String or System.Text.StringBuilder |
Decorate with ANSI. |
LPWSTR |
wchar_t* |
System.String or System.Text.StringBuilder |
Decorate with Unicode. |
LPCWSTR |
Const wchar_t* |
System.String or System.Text.StringBuilder |
Decorate with Unicode. |
FLOAT |
Float |
System.Single |
32 bits |
DOUBLE |
Double |
System.Double |
64 bits |
该示例程序为使用C++写的用于导出的函数库,后面的许多例子程序都基于该来进行。
平台调用拷贝字符串参数,如果需要的话可以将它们从.NET框架格式(Unicode)转换到非托管格式(ANSI)。因为托管的字符串是一成不变的(immutable),平台调用在函数返回的时候并不会将字符串从非托管内存拷贝回托管内存。
先了解一下C#中string和StringBuilder的区别:
string对象是不可改变的。每次使用 String 类中的方法之一或进行运算时(如赋值、拼接等)时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间。而 StringBuilder 则不会,在需要对字符串执行重复修改的情况下,与创建新的 String 对象相关的系统开销可能会非常昂贵。如果要修改字符串而不创建新的对象,则可以使用 System.Text.StringBuilder 类;例如,当在一个循环中将许多字符串连接在一起时,使用 StringBuilder 类可以提升性能。
同时string是System.String的别号,所以string和String是一回事。
C++函数原型(实质上有两个版本,一个是ansi版本MessageBoxA,另一个为unicode版本MessageBoxW,而MessageBox是对这两个版本的一个封装而已)
C++
int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption,
UINT uType);
⇒
C# publicclass LibWrap { // Declares managed prototypes for unmanaged functions. [DllImport( "User32.dll", EntryPoint="MessageBox",CharSet=CharSet.Auto)] publicstaticexternint MsgBox(int hWnd,string text,string caption,uint type); //这个会导致输出错误,因为以ansi格式封组参数,来使用这个参数的却是unicode版本的函数 [DllImport( "User32.dll", EntryPoint="MessageBoxW",CharSet=CharSet.Ansi )] publicstaticexternint MsgBox2(int hWnd,string text,string caption,uint type); //会抛出异常,因为函数名和字符集不匹配 [DllImport( "User32.dll", EntryPoint="MessageBox",CharSet=CharSet.Ansi, ExactSpelling=true )] publicstaticexternint MsgBox3(int hWnd,string text,string caption,uint type); } 调用 LibWrap.MsgBox(0,"Correct text","MsgBox Sample", 0);
以返回值的形式返回
char* TestStringAsResult();
==>
[DllImport("..\\LIB\\PinvokeLib.dll")] publicstaticexternstring TestStringAsResult(); 以参数的形式返回 typedef struct _MYSTRSTRUCT2 { char* buffer; UINT size; } MYSTRSTRUCT2; void TestStringInStructAnsi( MYSTRSTRUCT2* pStruct ) { printf( "\nAnsi buffer content: %s\n", pStruct->buffer ); // Assuming that the buffer is big enough. StringCbCatA( (STRSAFE_LPSTR) pStruct->buffer, pStruct->size, (STRSAFE_LPSTR)"++" ); } ⇒ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)] publicstruct MyStrStruct2 { publicstring buffer; publicint size; } [DllImport("..\\LIB\\PinvokeLib.dll")] publicstaticexternvoid TestStringInStructAnsi(ref MyStrStruct2 mss); 调用方法如下: StringBuilder buffer2 =new StringBuilder("content", 100); buffer2.Append((char)0); buffer2.Append('*', buffer2.Capacity - 8); MyStrStruct2 mss2; mss2.buffer = buffer2.ToString(); mss2.size = mss2.buffer.Length; LibWrap.TestStringInStructAnsi(ref mss2);
在该类的结构体中,使用了托管字符而不是StringBuilder缓存,是因为StringBuilder类型对象不能用在结构体中。但是这里实质上传给结构体的仍然是一个StringBuffer(因为这里需要修改结构体里字符串的内容),只是这里将它转换成了string类型以便用于结构体中而已。
C++
UINT WINAPI GetSystemDirectory( _Out_ LPTSTR lpBuffer, _In_ UINT uSize);
LPTSTR WINAPI GetCommandLine(void);
C# publicclass LibWrap { [DllImport("Kernel32.dll", CharSet=CharSet.Auto)] publicstaticexternint GetSystemDirectory(StringBuilder sysDirBuffer, int size); [DllImport("Kernel32.dll", CharSet=CharSet.Auto)] publicstaticextern IntPtr GetCommandLine(); } 调用示例 StringBuilder sysDirBuffer =new StringBuilder(256); LibWrap.GetSystemDirectory(sysDirBuffer, sysDirBuffer.Capacity); // Call GetCommandLine. IntPtr cmdLineStr = LibWrap.GetCommandLine(); string commandLine = Marshal.PtrToStringAuto(cmdLineStr);
GetCommandLine这里将返回的字符串指针也用IntPtr来替换,然后再用Marshal.PtrToStringAuto将该指针指向的字符内容读出来。而不是直接取出该内存块的数据,这是因为平台调默认情况下会主动去释放对象的内存空间,而GetCommandLine返回的是系统核心内存不能被随意释放,这部分参加默认封组方式。
C++原型 typedef struct _MYPERSON { char* first; char* last; } MYPERSON, *LP_MYPERSON; typedef struct _MYPERSON2 { MYPERSON* person; int age; } MYPERSON2, *LP_MYPERSON2; typedef struct _MYPERSON3 { MYPERSON person; int age; } MYPERSON3; typedef struct _MYARRAYSTRUCT { bool flag; int vals[ 3 ]; } MYARRAYSTRUCT; int TestStructInStruct(MYPERSON2* pPerson2); void TestStructInStruct3(MYPERSON3 person3); void TestArrayInStruct( MYARRAYSTRUCT* pStruct );
⇒C#原型 [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)] publicstruct MyPerson { publicstring first; publicstring last; } [StructLayout(LayoutKind.Sequential)] publicstruct MyPerson2 { public IntPtr person; publicint age; } [StructLayout(LayoutKind.Sequential)] publicstruct MyPerson3 { public MyPerson person; publicint age; } [StructLayout(LayoutKind.Sequential)] publicstruct MyArrayStruct { publicbool flag; [MarshalAs(UnmanagedType.ByValArray, SizeConst=3)] publicint[] vals; } publicclass LibWrap { // Declares a managed prototype for unmanaged function. [DllImport("..\\LIB\\PinvokeLib.dll")] publicstaticexternint TestStructInStruct(ref MyPerson2 person2); [DllImport("..\\LIB\\PinvokeLib.dll")] publicstaticexternint TestStructInStruct3(MyPerson3 person3); [DllImport("..\\LIB\\PinvokeLib.dll")] publicstaticexternint TestArrayInStruct(ref MyArrayStruct myStruct); }
这里面需要注意的是MyPerson2结构体的封组转换,该结构体内包含另一个结构体的指针,但是由于托管代码中不使用指针,因而需要使用IntPtr对象来代替,但同时该IntPtr必须为指向一块内存,使用示例如下:
MyPerson personName; personName.first = "Mark"; personName.last = "Lee"; MyPerson2 personAll; personAll.age = 30; IntPtr buffer = Marshal.AllocCoTaskMem(Marshal.SizeOf(personName)); Marshal.StructureToPtr(personName, buffer, false); personAll.person = buffer; int res = LibWrap.TestStructInStruct(ref personAll); MyPerson personRes =(MyPerson)Marshal.PtrToStructure(personAll.person, typeof(MyPerson)); Marshal.FreeCoTaskMem(buffer);
这段代码中,首先分配出一块非托管内存,然后将现有的数据拷贝到该内存块中,最后将该指针传递结构体对象的指针成员上。将结构体传入非托管函数处理后,可以再从指针成员中用PtrToStructure取出修改的数据,最后,需要手动释放前面分配的内存块。
其他的参见其他sample程序