参考网址:https://www.cnblogs.com/FongLuo/p/4512738.html

C#互操作系列文章:

  1. C# 互操作性入门系列(一):C#中互操作性介绍
  2. C# 互操作性入门系列(二):使用平台调用调用Win32 函数
  3. C# 互操作性入门系列(三):平台调用中的数据封送处理
  4. C# 互操作性入门系列(四):在C#中调用COM组件

本专题概要

  • 数据封送介绍
  • 封送Win32数据类型
  • 封送字符串的处理
  • 封送结构体的处理
  • 封送类的处理
  • 小结

一、数据封送介绍

看到这个专题时,大家的第一个疑问肯定是——什么是数据封送呢?(这系列专题中采用假设朋友的提问方式来解说概念,就是希望大家带着问题去学习本专题内容,以及大家在平时的学习过程中也可以采用这个方式,个人觉得这个方式可以使自己学习效率有所提高,即使这样在学习的过程可能会显得慢了,但是这种方式会对你所看过的知识点会有一个更深的印象。远比看的很快,最后却发现记住的没多少强,在这里分享下这个学习方式,认为可以接受的朋友可以在平时的学习中可以尝试下的,如果觉得不好的话,相信大家肯定也会有自己更好的学习方式的。)对于这个问题的解释是,数据封送是——在托管代码中对非托管函数进行互操作时,需要通过方法的参数和返回值在托管内存和非托管内存之间传递数据的过程,数据封送处理的过程是由CLR(公共语言运行时)的封送处理服务(即封送拆送器)完成的

封送拆送器主要进行3项任务:

    1. 将数据从托管类型转换为非托管类型,或从非托管类型转换为托管类型
    2. 将经过类型转换的数据从托管代码内存复制到非托管内存,或从非托管内存复制到托管内存
    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类型在托管代码中定义.然而本专题只是对数据封送做一个入门的介绍, 要真真掌握数据封送处理还要考虑很多其他的因素,这个就需要大家在平时工作中积累的。

[转]C# 互操作性入门系列(三):平台调用中的数据封送处理的更多相关文章

  1. C# 互操作性入门系列(三):平台调用中的数据封送处理

    好文章搬用工模式启动ing ..... { 文章中已经包含了原文链接 就不再次粘贴了 言明 改文章是一个系列,但只收录了2篇,原因是 够用了 } --------------------------- ...

  2. C# 互操作性入门系列(二):使用平台调用调用Win32 函数

    好文章搬用工模式启动ing ..... { 文章中已经包含了原文链接 就不再次粘贴了 言明 改文章是一个系列,但只收录了2篇,原因是 够用了 } --------------------------- ...

  3. [转]C# 互操作性入门系列(一):C#中互操作性介绍

    传送门 C#互操作系列文章: C# 互操作性入门系列(一):C#中互操作性介绍 C# 互操作性入门系列(二):使用平台调用调用Win32 函数 C# 互操作性入门系列(三):平台调用中的数据封送处理 ...

  4. [转]C# 互操作性入门系列(二):使用平台调用调用Win32 函数

    传送门 C#互操作系列文章: C# 互操作性入门系列(一):C#中互操作性介绍 C# 互操作性入门系列(二):使用平台调用调用Win32 函数 C# 互操作性入门系列(三):平台调用中的数据封送处理 ...

  5. [转]C# 互操作性入门系列(四):在C# 中调用COM组件

    传送门 C#互操作系列文章: C# 互操作性入门系列(一):C#中互操作性介绍 C# 互操作性入门系列(二):使用平台调用调用Win32 函数 C# 互操作性入门系列(三):平台调用中的数据封送处理 ...

  6. mybatis入门系列三之类型转换器

    mybatis入门系列三之类型转换器 类型转换器介绍 mybatis作为一个ORM框架,要求java中的对象与数据库中的表记录应该对应 因此java类名-数据库表名,java类属性名-数据库表字段名, ...

  7. ActiveMQ入门系列三:发布/订阅模式

    在上一篇<ActiveMQ入门系列二:入门代码实例(点对点模式)>中提到了ActiveMQ中的两种模式:点对点模式(PTP)和发布/订阅模式(Pub & Sub),详细介绍了点对点 ...

  8. Scrapy爬虫入门系列3 将抓取到的数据存入数据库与验证数据有效性

    抓取到的item 会被发送到Item Pipeline进行处理 Item Pipeline常用于 cleansing HTML data validating scraped data (checki ...

  9. Java入门系列:处理Json格式数据

    本节主要讲解: 1)json格式数据处理方法 2)第三方工具包的使用方法 3)java集合数据类型 [项目任务] 编写一个程序,显示未来的天气信息. [知识点解析] 为了方便后面代码的分析,先需要掌握 ...

随机推荐

  1. FA转发地址

    1.FA地址诞生背景和作用 FA 是Forwarding Address的简写.FA是ASBR通告的TYPE 5 LSA中的字段,它的作用是告诉OSPF域内的路由器如何能够更快捷地到达LSA 5所通告 ...

  2. 「CF1208G」 Polygons

    「CF1208G」 Polygons 似乎我校神犇在很久以前和我提过这题? 首先有一点显而易见:这 \(k\) 个多边形肯定至少有一个公共的顶点.假设我们将此点定义为起点. 那么对于一个正 \(n\) ...

  3. [004] .NET 的现状和未来

    我们总是能听到一些人说,.NET 不行.学 .NET 没发展前途之类的言论,有的童鞋听多了便也开始怀疑自己选择的 .NET 方向是不是错了. 不得不承认,在 .NET 没有实现跨平台之前,市场行情确实 ...

  4. Jmeter性能测试指标分析

    一.Aggregate Report 是 JMeter 常用的一个 Listener,中文被翻译为"聚合报告 如果大家都是做Web应用的性能测试,例如访问百度请求为例,线程10,循环10次, ...

  5. python + pytest基本使用方法(拓展库)

    一.测试钩子配置文件 import pytest# conftest.py 是pytest特有的本地测试配置文件;# 既可以用来设置项目级别的Fixture,也可用来导入外部插件,还可以指定钩子函数# ...

  6. 03 高性能IO模型:采用多路复用机制的“单线程”Redis

    本篇重点 三个问题: "Redis真的只有单线程吗?""为什么用单线程?""单线程为什么这么快?" "Redis真的只有单线程吗? ...

  7. Linux核心目录结构

    ------------恢复内容开始------------ 目录 含义及作用 /usr/bin 普通用户二进制命令目录 /usr/sbin root管理员用户使用的二进制命令目录 /boot 内核程 ...

  8. BZOJ2457 双端队列 题解

    本题直接求解十分困难,因为在不知道整个序列的数字规律时当前所作决策都无法保证最优性. 考虑正难则反,题目转化为将一个非降序列分成尽量少的几段,让每段对应原问题的双端队列. 先将原数组排序,由于原数组下 ...

  9. 流暢的pyhton4---數據庫備份

    一.linux數據庫備份腳本 1.数据库备份,命令如下: ./pg_dump -h localhost -p 5432 -U postgres -W -F c -b -v -f "/opt/ ...

  10. Python基础之用PyQt5创建menu

    前一篇文章中,我们已经安装了PyQt5,并且已经测试过可用.那么接下来第一步开始学习如何创建菜单. 第一步:在想要运行py的地方右击External Tools-->designer,打开des ...