C# 互操作性入门系列(三):平台调用中的数据封送处理
C#互操作系列文章:
- C#互操作性入门系列(一):C#中互操作性介绍
- C#互操作性入门系列(二):使用平台调用调用Win32 函数
- C#互操作性入门系列(三):平台调用中的数据封送处理
- C#互操作性入门系列(四):在C# 中调用COM组件
本专题概要
- 数据封送介绍
- 封送Win32数据类型
- 封送字符串的处理
- 封送结构体的处理
- 封送类的处理
- 小结
一、数据封送介绍
看到这个专题时,大家的第一个疑问肯定是——什么是数据封送呢?(这系列专题中采用假设朋友的提问方式来解说概念,就是希望大家带着问题去学习本专题内容,以及大家在平时的学习过程中也可以采用这个方式,个人觉得这个方式可以使自己学习效率有所提高,即使这样在学习的过程可能会显得慢了,但是这种方式会对你所看过的知识点会有一个更深的印象。远比看的很快,最后却发现记住的没多少强,在这里分享下这个学习方式,认为可以接受的朋友可以在平时的学习中可以尝试下的,如果觉得不好的话,相信大家肯定也会有自己更好的学习方式的。)对于这个问题的解释是,数据封送是——在托管代码中对非托管函数进行互操作时,需要通过方法的参数和返回值在托管内存和非托管内存之间传递数据的过程,数据封送处理的过程是由CLR(公共语言运行时)的封送处理服务(即封送拆送器)完成的。
封送拆送器主要进行3项任务:
- 将数据从托管类型转换为非托管类型,或从非托管类型转换为托管类型
- 将经过类型转换的数据从托管代码内存复制到非托管内存,或从非托管内存复制到托管内存
- 调用完成后,释放封送处理过程中分配的内存
二、封送Win32数据类型
对非托管代码进行互操作时,一定会有数据的封送处理。然而封送时需要处理的数据类型分为两种——可直接复制到本机结构中的类型(blittable)和非直接复制到本机结构中的类型(non-bittable)。下面就这两种数据类型分别做一个介绍。
2.1 可直接复制到本机结构中的类型
由于在托管代码和非托管代码中,数据类型在托管内存和非托管内存的表示形式不一样,因为这样的原因,所以我们需要对数据进行封送处理,以至于在托管代码中调用非托管函数时,把正确的传入参数传递给非托管函数和把正确的返回值返回给托管代码中。然而,并不是所有数据类型在两者内存的表现形式不一样的,这时候我们把在托管内存和非托管内存中有相同表现形式的数据类型称为——可直接复制到本机结构中的类型,这些数据类型不需要封送拆送器进行任何特殊的处理就可以在托管和非托管代码之间传递, 下面列出一些课直接复制到本机结构中的简单数据类型:
Windows 数据类型 |
非托管数据类型 |
托管数据类型 |
托管数据类型解释 |
BYTE/Uchar/UInt8 |
unsigned char |
System.Byte |
无符号8位整型 |
Sbyte/Char/Int8 |
char |
System.SByte |
有符号8位整型 |
Short/Int16 |
short |
System.Int16 |
有符号16位整型 |
USHORT/WORD/UInt16/WCHAR |
unsigned short |
System.UInt16 |
无符号16位整型 |
Bool/HResult/Int/Long |
long/int |
System.Int32 |
有符号32位整型 |
DWORD/ULONG/UINT |
unsigned long/unsigned int |
System.UInt32 |
无符号32位整型 |
INT64/LONGLONG |
_int64 |
System.Int64 |
有符号64位整型 |
UINT64/DWORDLONG/ULONGLONG |
_uint64 |
System.UInt64 |
无符号64位整型 |
INT_PTR/hANDLE/wPARAM |
void*/int或_int64 |
System.IntPtr |
有符号指针类型 |
HANDLE |
void* |
System.UIntPtr |
无符号指针类型 |
FLOAT |
float |
System.Single |
单精度浮点数 |
DOUBLE |
double |
System.Double |
双精度浮点数 |
除了上表列出来的简单类型之外,还有一些复制类型也属于可直接复制到本机结构中的数据类型:
(1) 数据元素都是可直接复制到本机结构中的一元数组,如整数数组,浮点数组等
(2)只包含可直接复制到本机结构中的格式化值类型
(3)成员变量全部都是可复制到本机结构中的类型且作为格式化类型封送的类
上面提到的格式化指的是——在类型定义时,成员的内存布局在声明时就明确指定的类型。在代码中用StructLayout属性修饰被指定的类型,并将StructLayout的LayoutKind属性设置为Sequential或Explicit,例如:
using System.Runtime.InteropServices; // 下面的结构体也属于可直接复制到本机结构中的类型 [StructLayout(LayoutKind.Sequential)] public struct Point { public int x; public int y; }
2.2 非直接复制到本机结构中的类型
如果一个类型不是可直接复制到本机结构中的类型,那么它就是非直接复制到本机结构中的类型。由于一些类型在托管内存和非托管内存的表现形式不一样,所以对于这种类型,封送器需要对它们进行相应的类型转换之后再复制到被调用的函数中,下面列出一些非直接复制到本机结构中的数据类型:
Windows 数据类型 |
非托管数据类型 |
托管数据类型 |
托管数据类型解释 |
Bool |
bool |
System.Boolean |
布尔类型 |
WCHAR/TCHAR |
char/ wchar_t |
System.Char |
ANSI字符/Unicode字符 |
LPCSTR/LPCWSTR/LPCTSTR/LPSTR/LPWSTR/LPTSTR |
const char*/const wchar_t*/char*/wchar_t* |
System.String |
ANSI字符串/Unicode字符串,如果非托管代码不需要更新此字符串时,此时用String类型在托管代码中声明字符串类型 |
LPSTR/LPWSTR/LPTSTR |
Char*/wchar_t* |
System.StringBuilder |
ANSI字符串/Unicode字符串,如果非托管代码需要更新此字符串,然后把更新的字符串传回托管代码中,此时用StringBuilder类型在托管代码中声明字符串 |
除了上表中列出的类型之外,还有很多其他类型属于非直接复制到本机结构中的类型,例如其他指针类型和句柄类型等。理解了blittable和non-blittable类型的区别之后,就可以在互操作过程更好地处理数据的封送,下面就具体的一些数据类型的封送问题做一个简单介绍
三、封送字符串的处理
在上一个专题中,我们已经涉及到字符串的封送问题了(上一个专题中使用了将字符串作为In参数传递给Win32 MessageBox 函数,具体可以查看上一个专题) 。所以在这部分将介绍——封送作为返回值的字符串,下面是一段演示代码,代码中主要是调用Win32 GetTempPath函数来获得返回返回临时路径,此时拆送器就需要把返回的字符串封送回托管代码中。
// 托管函数中的返回值封送回托管函数的例子 class Program { // Win32 GetTempPath函数的定义如下: //DWORD WINAPI GetTempPath( // _In_ DWORD nBufferLength, // _Out_ LPTSTR lpBuffer //); // 主要是注意如何在托管代码中定义该函数原型
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError=true)] public static extern uint GetTempPath(int bufferLength, StringBuilder buffer); static void Main(string[] args) { StringBuilder buffer = new StringBuilder(300); uint tempPath=GetTempPath(300, buffer); string path = buffer.ToString(); if (tempPath == 0) { int errorcode =Marshal.GetLastWin32Error(); Win32Exception win32expection = new Win32Exception(errorcode); Console.WriteLine("调用非托管函数发生异常,异常信息为:" +win32expection.Message); } Console.WriteLine("调用非托管函数成功。"); Console.WriteLine("Temp 路径为:" + buffer); Console.Read(); } }
运行结果为:
四、封送结构体的处理
在我们实际调用Win32 API函数时,经常需要封送结构体和类等复制类型,下面就以Win32 函数GetVersionEx为例子来演示如何对作为参数的结构体进行封送处理。为了在托管代码中调用非托管代码,首先我们就要知道非托管函数的定义,下面是GetVersionEx非托管定义(更多关于该函数的信息可以参看MSDN链接:http://msdn.microsoft.com/en-us/library/ms885648.aspx ):
BOOL GetVersionEx(
LPOSVERSIONINFO lpVersionInformation
);
参数lpVersionInformation是一个指向 OSVERSIONINFO结构体的指针类型,所以我们在托管代码中为函数GetVersionEx函数之前,必须知道 OSVERSIONINFO结构体的非托管定义,然后再在托管代码中定义一个等价的结构体类型作为参数。以下是OSVERSIONINFO结构体的非托管定义:
typedef struct _OSVERSIONINFO{ DWORD dwOSVersionInfoSize; //在使用GetVersionEx之前要将此初始化为结构的大小 DWORD dwMajorVersion; //系统主版本号 DWORD dwMinorVersion; //系统次版本号 DWORD dwBuildNumber; //系统构建号 DWORD dwPlatformId; //系统支持的平台 TCHAR szCSDVersion[128]; //系统补丁包的名称 WORD wServicePackMajor; //系统补丁包的主版本 WORD wServicePackMinor; //系统补丁包的次版本 WORD wSuiteMask; //标识系统上的程序组 BYTE wProductType; //标识系统类型 BYTE wReserved; //保留,未使用 } OSVERSIONINFO;
知道了OSVERSIONINFO结构体在非托管代码中的定义之后, 现在我们就需要在托管代码中定义一个等价的结构,并且要保证两个结构体在内存中的布局相同。托管代码中的结构体定义如下:
// 因为Win32 GetVersionEx函数参数lpVersionInformation是一个指向 OSVERSIONINFO的数据结构 // 所以托管代码中定义个结构体,把结构体对象作为非托管函数参数 [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)] public struct OSVersionInfo { public UInt32 OSVersionInfoSize; // 结构的大小,在调用方法前要初始化该字段 public UInt32 MajorVersion; // 系统主版本号 public UInt32 MinorVersion; // 系统此版本号 public UInt32 BuildNumber; // 系统构建号 public UInt32 PlatformId; // 系统支持的平台 // 此属性用于表示将其封送成内联数组 [MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)] public string CSDVersion; // 系统补丁包的名称 public UInt16 ServicePackMajor; // 系统补丁包的主版本 public UInt16 ServicePackMinor; // 系统补丁包的次版本 public UInt16 SuiteMask; //标识系统上的程序组 public Byte ProductType; //标识系统类型 public Byte Reserved; //保留,未使用 }
从上面的定义可以看出, 托管代码中定义的结构体有以下三个方面与非托管代码中的结构体是相同的:
- 字段声明的顺序
- 字段的类型
- 字段在内存中的大小
并且在上面结构体的定义中,我们使用到了 StructLayout 属性,该属性属于System.Runtime.InteropServices命名空间(所以在使用平台调用技术必须添加这个额外的命名空间)。这个类的作用就是允许开发人员显式指定结构体或类中数据字段的内存布局,为了保证结构体中的数据字段在内存中的顺序与定义时一致,所以指定为 LayoutKind.Sequential(该枚举也是默认值)。 下面就具体看看在托管代码中调用的代码:
using System; using System.ComponentModel; using System.Runtime.InteropServices; namespace 封送结构体的处理 { class Program { // 对GetVersionEx进行托管定义 // 为了传递指向结构体的指针并将初始化的信息传递给非托管代码,需要用ref关键字修饰参数
// 这里不能使用out关键字,如果使用了out关键字,CLR就不会对参数进行初始化操作,这样就会导致调用失败
[DllImport("Kernel32",CharSet=CharSet.Unicode,EntryPoint="GetVersionEx")] private static extern Boolean GetVersionEx_Struct(ref OSVersionInfo osVersionInfo); // 因为Win32 GetVersionEx函数参数lpVersionInformation是一个指向 OSVERSIONINFO的数据结构 // 所以托管代码中定义个结构体,把结构体对象作为非托管函数参数 [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)] public struct OSVersionInfo { public UInt32 OSVersionInfoSize; // 结构的大小,在调用方法前要初始化该字段 public UInt32 MajorVersion; // 系统主版本号 public UInt32 MinorVersion; // 系统此版本号 public UInt32 BuildNumber; // 系统构建号 public UInt32 PlatformId; // 系统支持的平台 // 此属性用于表示将其封送成内联数组 [MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)] public string CSDVersion; // 系统补丁包的名称 public UInt16 ServicePackMajor; // 系统补丁包的主版本 public UInt16 ServicePackMinor; // 系统补丁包的次版本 public UInt16 SuiteMask; //标识系统上的程序组 public Byte ProductType; //标识系统类型 public Byte Reserved; //保留,未使用 } // 获得操作系统信息 private static string GetOSVersion() { // 定义一个字符串存储版本信息 string versionName = string.Empty; // 初始化一个结构体对象 OSVersionInfo osVersionInformation = new OSVersionInfo(); // 调用GetVersionEx 方法前,必须用SizeOf方法设置结构体中OSVersionInfoSize 成员 osVersionInformation.OSVersionInfoSize = (UInt32)Marshal.SizeOf(typeof(OSVersionInfo)); // 调用Win32函数 Boolean result = GetVersionEx_Struct(ref osVersionInformation); if (!result) { // 如果调用失败,获得最后的错误码 int errorcode = Marshal.GetLastWin32Error(); Win32Exception win32Exc = new Win32Exception(errorcode); Console.WriteLine("调用失败的错误信息为: " + win32Exc.Message); // 调用失败时返回为空字符串 return string.Empty; } else { Console.WriteLine("调用成功"); switch (osVersionInformation.MajorVersion) { // 这里仅仅讨论 主版本号为6的情况,其他情况是一样讨论的 case 6: switch (osVersionInformation.MinorVersion) { case 0: if (osVersionInformation.ProductType == (Byte)0) { versionName = " Microsoft Windows Vista"; } else { versionName = "Microsoft Windows Server 2008"; // 服务器版本 } break; case 1: if (osVersionInformation.ProductType == (Byte)0) { versionName = " Microsoft Windows 7"; } else { versionName = "Microsoft Windows Server 2008 R2"; } break; case 2: versionName = "Microsoft Windows 8"; break; } break; default: versionName = "未知的操作系统"; break; } return versionName; } } static void Main(string[] args) { string OS=GetOSVersion(); Console.WriteLine("当前电脑安装的操作系统为:{0}", OS); Console.Read(); } } }
运行结果为:
附上微软操作系统名和版本号的对应关系,大家可以参考下面的表对上面代码进行其他的讨论:
操作系统 |
版本号 |
Windows 8 |
6.2 |
Windows 7 |
6.1 |
Windows Server 2008 R2 |
6.1 |
Windows Server 2008 |
6.0 |
Windows Vista |
6.0 |
Windows Server 2003 R2 |
5.2 |
Windows Server 2003 |
5.2 |
Windows XP |
5.1 |
Windows 2000 |
5.0 |
五、封送类的处理
下面直接通过GetVersionEx函数进行封送类的处理的例子,具体代码如下:
using System; using System.ComponentModel; using System.Runtime.InteropServices; namespace 封送类的处理 { class Program { // 对GetVersionEx进行托管定义 // 由于类的定义中CSDVersion为String类型,String是非直接复制到本机结构类型, // 所以封送拆送器需要进行复制操作。 // 为了是非托管代码能够获得在托管代码中对象设置的初始值(指的是OSVersionInfoSize字段,调用函数前首先初始化该值), // 所以必须加上[In]属性;函数返回时,为了将结果复制到托管对象中,必须同时加上 [Out]属性 // 这里不能是用ref关键字,因为 OsVersionInfo是类类型,本来就是引用类型,如果加ref 关键字就是传入的为指针的指针了,这样就会导致调用失败
[DllImport("Kernel32", CharSet = CharSet.Unicode, EntryPoint = "GetVersionEx")] private static extern Boolean GetVersionEx_Struct([In, Out] OSVersionInfo osVersionInfo); // 获得操作系统信息 private static string GetOSVersion() { // 定义一个字符串存储操作系统信息 string versionName = string.Empty; // 初始化一个类对象 OSVersionInfo osVersionInformation = new OSVersionInfo(); // 调用Win32函数 Boolean result = GetVersionEx_Struct(osVersionInformation); if (!result) { // 如果调用失败,获得最后的错误码 int errorcode = Marshal.GetLastWin32Error(); Win32Exception win32Exc = new Win32Exception(errorcode); Console.WriteLine("调用失败的错误信息为: " + win32Exc.Message); // 调用失败时返回为空字符串 return string.Empty; } else { Console.WriteLine("调用成功"); switch (osVersionInformation.MajorVersion) { // 这里仅仅讨论 主版本号为6的情况,其他情况是一样讨论的 case 6: switch (osVersionInformation.MinorVersion) { case 0: if (osVersionInformation.ProductType == (Byte)0) { versionName = " Microsoft Windows Vista"; } else { versionName = "Microsoft Windows Server 2008"; // 服务器版本 } break; case 1: if (osVersionInformation.ProductType == (Byte)0) { versionName = " Microsoft Windows 7"; } else { versionName = "Microsoft Windows Server 2008 R2"; } break; case 2: versionName = "Microsoft Windows 8"; break; } break; default: versionName = "未知的操作系统"; break; } return versionName; } } static void Main(string[] args) { string OS = GetOSVersion(); Console.WriteLine("当前电脑安装的操作系统为:{0}", OS); Console.Read(); } } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public class OSVersionInfo { public UInt32 OSVersionInfoSize = (UInt32)Marshal.SizeOf(typeof(OSVersionInfo)); public UInt32 MajorVersion = 0; public UInt32 MinorVersion = 0; public UInt32 BuildNumber = 0; public UInt32 PlatformId = 0; // 此属性用于表示将其封送成内联数组 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string CSDVersion = null; public UInt16 ServicePackMajor = 0; public UInt16 ServicePackMinor = 0; public UInt16 SuiteMask = 0; public Byte ProductType = 0; public Byte Reserved; } }
运行结果还是和上面使用结构体定义的一样,还是附上下图吧:
六、小结
本专题主要介绍了几种类型的数据封送处理, 对于封送处理的一句话概括就是——保证托管代码中定义的数据在内存中的布局与非托管代码中的内存布局相同,专题中也列出了一些简单类型在非托管代码和托管代码中定义的对应关系,对于一些没有列出来的指针类型或回调函数等可以使用万能的IntPtr类型在托管代码中定义.然而本专题只是对数据封送做一个入门的介绍, 要真真掌握数据封送处理还要考虑很多其他的因素,这个就需要大家在平时工作中积累的。