掌握了.NET平台下的互操作性技术可以帮助我们在**.NET中调用非托管的dll和COM组件**。.NET是建立在操作系统的之上的一个开发框架,其中.NET 类库中的类也是对Windows API的抽象封装,然而.NET类库不可能对所有Windows API进行封装,当.NET中没有实现某个功能的类,然而该功能在Windows API被实现了,此时我们完全没必要去自己在.NET中自定义个类,这时候就可以调用Windows API 中的函数来实现,此时就涉及到托管代码与非托管代码的交互,此时就需要使用到互操作性的技术来实现托管代码和非托管代码更好的交互。.NET 平台下提供了3种互操作性的技术:
1. Platform Invoke(P/Invoke),即平台调用,主要用于调用C库函数和Windows API
2. C++ Introp, 主要用于Managed C++(托管C++)中调用C++类库
3. COM Interop, 主要用于在.NET中调用COM组件和在COM中使用.NET程序集。




平台调用可以帮助我们实现在.NET平台下(也就是指用C#、VB.net语言写的应用程序下)可以调用非托管函数(指定的是C/C++语言写的函数)。这样如果我们在.NET平台下实现的功能有现有的C/C++ 函数实现了这样的功能,这时候我们完全没必要自己再用托管语言(如C#、vb.net)去实现一个这样的功能,这时候我们应该想到 “拿来主义”,直接使用平台调用技术调用C/C++ 实现的函数。然而在实际应用中,使用平台调用技术来调用Win32 API较为普遍,

上图中 我用红色标示出 MessageBox 有两个版本,而MessageBoxA 代表的就是ANSI版本,而MessageBoxW 代笔的就是Unicode版本,这也是上面所说的依据。下面就看看 MessageBox的C++声明的(更多的函数的定义大家可以从MSDN中找到



捕捉由托管定义导致的异常演示代码:
捕获由Win32函数本身返回异常:

捕获由Win32函数本身返回异常的演示代码如下:
using System;
using System.ComponentModel;
// 使用平台调用技术进行互操作性之前,首先需要添加这个命名空间
using System.Runtime.InteropServices;
namespace 处理Win32函数返回的错误
{
class Program
{
// Win32 API
// DWORD WINAPI GetFileAttributes(
// In LPCTSTR lpFileName
//);
// 在托管代码中对非托管函数进行声明 SetLastError=true
[DllImport("Kernel32.dll",SetLastError=true,CharSet=CharSet.Unicode)]
public static extern uint GetFileAttributes(string filename);
static void Main(string[] args)
{
// 试图获得一个不存在文件的属性
// 此时调用Win32函数会发生错误
GetFileAttributes("FileNotexist.txt");
// 在应用程序的Bin目录下存在一个test.txt文件,此时调用会成功
//GetFileAttributes("test.txt");
// 获得最后一次获得的错误
int lastErrorCode = Marshal.GetLastWin32Error();
// 将Win32的错误码转换为托管异常
//Win32Exception win32exception = new Win32Exception();
Win32Exception win32exception = new Win32Exception(lastErrorCode);
if (lastErrorCode != 0)
{
Console.WriteLine("调用Win32函数发生错误,错误信息为 : {0}", win32exception.Message);
}
else
{
Console.WriteLine("调用Win32函数成功,返回的信息为: {0}", win32exception.Message);
}
Console.Read();
}
}
}


数据封送——在托管代码中对非托管函数进行互操作时,需要通过方法的参数和返回值在托管内存和非托管内存之间传递数据的过程,数据封送处理的过程是由CLR(公共语言运行时)的封送处理服务(即封送拆送器)完成的。

二、封送Win32数据类型
对非托管代码进行互操作时,一定会有数据的封送处理。然而封送时需要处理的数据类型分为两种——可直接复制到本机结构中的类型(blittable)和非直接复制到本机结构中的类型(non-bittable)。下面就这两种数据类型分别做一个介绍。
2.1 可直接复制到本机结构中的类型
由于在托管代码和非托管代码中,数据类型在托管内存和非托管内存的表示形式不一样,因为这样的原因,所以我们需要对数据进行封送处理,以至于在托管代码中调用非托管函数时,把正确的传入参数传递给非托管函数和把正确的返回值返回给托管代码中。然而,并不是所有数据类型在两者内存的表现形式不一样的,这时候我们把在托管内存和非托管内存中有相同表现形式的数据类型称为——可直接复制到本机结构中的类型,这些数据类型不需要封送拆送器进行任何特殊的处理就可以在托管和非托管代码之间传递, 下面列出一些课直接复制到本机结构中的简单数据类型:

除了上表列出来的简单类型之外,还有一些复制类型也属于可直接复制到本机结构中的数据类型:
(1) 数据元素都是可直接复制到本机结构中的一元数组,如整数数组,浮点数组等
(2)只包含可直接复制到本机结构中的格式化值类型
(3)成员变量全部都是可复制到本机结构中的类型且作为格式化类型封送的类



四、封送结构体的处理
在我们实际调用Win32 API函数时,经常需要封送结构体和类等复制类型,下面就以Win32 函数GetVersionEx为例子来演示如何对作为参数的结构体进行封送处理。为了在托管代码中调用非托管代码,首先我们就要知道非托管函数的定义,下面是GetVersionEx非托管定义


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();
}
}
}

五、封送类的处理
下面直接通过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;
}
}

**
**

using System;
// 添加额外的命名空间
using Microsoft.Office.Interop.Word;
namespace COM互操作性
{
class Program
{
static void Main(string[] args)
{
// 调用COM对象来创建Word文档
CreateWordDocument();
}
private static void CreateWordDocument()
{
// 启动Word并使Word可见
Application wordApp = new Application() { Visible = true };
// 新建Word文档
wordApp.Documents.Add();
Document wordDoc = wordApp.ActiveDocument;
Paragraph para = wordDoc.Paragraphs.Add();
para.Range.Text = "欢迎你;
// 保存文档
object filename = @"D:\learninghard.doc";
wordDoc.SaveAs2(filename);
// 关闭Word
wordDoc.Close();
wordApp.Application.Quit();
}
}
}
此时在所指定的文件目录中就可以看到你刚才创建的Word文档了。通过COM互操作的技术我们可以Office的自动化操作。

然而我们也可以使用Visual Studio中内置的支持来完成为COM类型库创建互操作程序集的工作,我们只需要在VS中为.NET 项目添加对应的COM组件的引用,此时VS就会自动将COM类型库中的COM类型库转化为程序集中的元数据,并在项目的Bin目录下生成对于的互操作程序集,所以在VS中添加COM引用,其实最后程序中引用的是互操作程序集,然后通过RCW来对COM组件进行调用。 然而对于Office中的Microsoft.Office.Interop.Wordd.dll,这个程序集也是互操作程序集,但是它又是主互操作程序集,即PIA(Primary Interop Assemblies)。主互操作程序集是一个由供应商提供的唯一的程序集,为了生成主互操作程序集,可以在使用TlbImp命令是打开 /primary 选项。看到这里,朋友们肯定有这样的疑问:PIA与普通程序集到底有什么区别呢?——区别就是PIA除了包含了COM组件定义的数据类型外,还包含了一些特殊的信息,如公钥,COM类型库的提供者等信息。然而 为什么需要主互操作程序集的呢 ? 对于这个问题的答案就是——主互操作程序集可以帮助我们解决部署程序时,引用互操作程序集版本不一致的问题。(如果开发人员会为一个COM组件类型库生成多个互操作程序集,项目中引用的互操作程序集版本与部署时的互操作程序集版本不一致的问题,有了互操作程序集时,我们可以直接引用官方提供主互操作程序集。)
四、错误处理
知道了如何调用COM组件之后,大家或许会问:如果调用COM对象的方法失败时怎么去获取失败的信息呢?对于这个疑问,错误的处理的方法和我们平常托管代码中的处理方式是一样的,下面就具体看看是如何获取错误信息的,下面这段代码的功能是——打开一个现有的Word文档并插入相应的文本,当指定的Word文档不存在时,此时就会出现调用COM对象的Open方法失败的情况,具体代码如下:
using System;
using Microsoft.Office.Interop.Word;
using System.IO;
using System.Runtime.InteropServices;
namespace COM互操作中的错误处理
{
class Program
{
static void Main(string[] args)
{
// 打开存在的文档插入文本
string wordPath = @“D:\test.docx”;
OpenWordDocument(wordPath);
Console.Read();
}
// 向现有文档插入文本
private static void OpenWordDocument(string wordPath)
{
// 启动Word 应用程序
Application wordApp = new Application() { Visible = true };
Document wordDoc=null;
try
{
// 如果文档不存在时,就会出现调用COM对象失败的情况
// 打开Word文档
wordDoc = wordApp.Documents.Open(wordPath);
// 向Word中插入文本
Range wordRange = wordDoc.Range(0, 0);
wordRange.Text = "这是插入的文本";
// 保存文档
wordDoc.Save();
}
catch(Exception ex)
{
// 获得异常相对应的HRESULT值
// 因为COM中根据方法返回的HRESULT来判断调用是否成功的
int HResult = Marshal.GetHRForException(ex);
// 设置控制台的前景色,即输出文本的颜色
Console.ForegroundColor = ConsoleColor.Red;
// 下面把HRESULT值以16进制输出
Console.WriteLine("调用抛出异常,异常类型为:{0}, HRESULT= 0x{1:x}", ex.GetType().Name, HResult);
Console.WriteLine("异常信息为:" + ex.Message.Replace('\r', ' '));
}
finally
{
// 关闭文档并
if (wordDoc != null)
{
wordDoc.Close();
}
// 退出Word程序
wordApp.Quit();
}
}
}
}
