自从2000年微软.NET平台问世以来,全球已经有超过四百万开发人员使用.NET平台进行软件开发。对于.NET来说,这无疑是一个巨大的成功。这不仅仅体现在商业上的成功,其核心价值在于.NET为基于微软Windows平台的软件开发过程提供了一种新颖、高效的编程模型。在该模型下,开发人员能够更容易地将精力集中在其特定的开发情景中,而不用过多地关注消息循环、窗口过程等操作系统底层的处理。
另一方面,由于历史的原因,在.NET出现之前,开发人员已经编写了大量经过严格测试且可复用的非托管代码。它们以C库函数、C++类库以及COM组件的形式存在于诸多应用程序和框架之中,并承担着非常重要的角色。但由于在托管和非托管对象模型之间,数据类型、方法签名和错误处理机制都存在很大差异,从而使两种编程模型之间的代码互用和移植更加复杂。因此,在很长一段时期内,开发人员必须面对.NET与久经考验的"遗留代码(legacy code)"长期并存的局面。
公共语言运行库(Common Language Runtime,简称CLR)提供了一系列能够使托管代码与非托管代码进行交互操作的解决方案。其中主要包含3类互操作技术:
平台调用技术(P/Invoke):主要用于处理在托管代码中调用C库函数及Win32 API函数等非托管函数的情形。
C++ Interop:适用于在托管代码与C++类库、核心算法库之间进行高效、灵活的互操作过程。一方面托管代码可以通过包装类机制使用C++类库,另一方面非托管代码可以通过包装模板机制使用托管对象。
COM Interop:该技术用于处理托管代码与COM之间的交互过程。托管代码通过运行库可调用包装(RCW)使用非托管COM组件。反过来,非托管COM客户端可以通过COM可调用包装(CCW)使用托管程序集。
1平台调用技术(P/Invoke)
Win32 API是C语言(注意,不是C++语言,尽管C语言是C++语言的子集)函数集。C#语言与C语言是完全不同的(除了语法上比较像),所以,要想用C#语言调用C语言的Win32 API,要费上一番周折。首先我们就要准备一些基础知识。
Win32 API函数是Windows的核心,比如我们看到的窗体、按钮、对话框什么的,都是依靠Win32函数"画"在屏幕上的,由于这些控件(有时也称组件)都用于用户与Windows进行交互,所以控制这些控件的Win32 API函数称为"用户界面"函数(User Interface Win32 API),简称UI函数;还有一些函数,并不用于交互,比如管理当前系统正在运行的进程、硬件系统状态的监视等等……这些函数只有一套,但是可以被所有的Windows程序调用(只要这个程序的权限足够高),简而言之,API是为程序所共享的。为了达到所有程序能共享一套API的目的,Windows采用了"动态链接库"的办法。之所以叫"动态链接库",是因为这样的函数库的调用方式是"随用随取"而不是像静态链接库那样"用不用都要带上"。
Win32 API函数是放在Windows系统的核心库文件中的,这些库在硬盘里的存储形式是.dll文件。我们常用到的dll文件是user32.dll和kernel32.dll两个文件,还有其它一些dll文件也非常重要,大家要在实践中多积累经验。
我们知道Win32 API函数是放在dll文件中了,但新问题又来了——我们怎么调用它们呢?这些dll文件是用C语言写的,源代码经C语言编译器编译之后,会以二进制可执行代码形式存放在这些dll文件中,就好像苹果被打碎机打成果酱后装在罐子里一样——你再也分不清哪个是你GF给你的,哪个是你老妈给你的一样。为了能让程序使用这些函数,微软在发布每个新的操作系统的时候,也会放出这个系统的SDK。
现在,我们已经找到了问题的关键点:如何用.NET平台上的C#语言来调用Win32平台上的dll文件。答案非常简单:使用DllImport特性。
下面,就让我们写一个小程序,试一试如何用C#语言和DllImport特性来调用Win32 API。
using System;
using System.Runtime.InteropServices;
class Program
{
[DllImport("User32.dll")]
public static extern int MessageBox(int h, string m, string c, int type);
static int Main()
{
MessageBox(0, "Hello Win32 API", "水之真谛", 4);
Console.ReadLine();
return 0;
}
}
2 C++调用
c++经过这么多年的发展已经积累了大量的动态连接库,如果能够在.net环境里应用这些函数库,
可以很大的提高整个应用的开发速度。
使用c++编程的人员肯定对指针不会感到陌生,由于c++中的函数接口好多都可能定义成位指针,
而c#中只有在声明为unsafe code中才能够使用指针。如果想让c++的DLL支持在C#中调用,
那么在C++接口的声明中需要使用下面的这种格式:
extern "C" __declspec(dllexport) void __stdcall popMessage(char* message)
{
MessageBox(NULL, message, "C message from C#!", MB_OK);
}
并且在c#类声明中使用如下的导入编译好的DLL,例如:
[ DllImport( "test.dll", CallingConvention=CallingConvention.Cdecl )]
public static extern void Message(string theMessage);
3 COM调用
.NET framework 是从COM的一种自然地进步,因为这两个模型共享了许多中心的主题,包括组件重用和语言中立。为了支持向后兼容,COM interop提供了不需要修改现有组件而能访问现有COM组件的方法。可以通过使用COM interop工具导入相关的COM类型来合并COM组件到.NET Framework的应用中。一旦导入,COM的类型就可以使用了。
COM interop 同时也提供了向前兼容使得COM的客户可以像访问其他的COM对象一样访问托管的代码,COM interop又一次的提供了所谓的无缝从程序集中导出元数据(metadata)到类型库并且像传统COM组件一样注册托管组件的方法。无论是导出还是导入工具处理的结果都与COM规范一致。在运行时,如果需要的话common language runtime在COM对象和托管代码之间列集(marshals)数据
以下演示一个封装好的com调用类
public class ComManage
{
public string CLSID { set; private get; }
public Dictionary
public object[] Arguments { set; get; }
private Type _dynamicType;
private object _dynamicObject;
///
/// 方法调用
///
///
///
public object MethodInvoke(string methodName)
{
if (_dynamicType == null)
{
_dynamicType = Type.GetTypeFromCLSID(new Guid(CLSID));
}
if (_dynamicObject == null)
{
_dynamicObject = Activator.CreateInstance(_dynamicType);
}
//给属性赋值
foreach (KeyValuePair
{
_dynamicType.InvokeMember(property.Key, BindingFlags.SetProperty, null, _dynamicObject, new object[] { property.Value });
}
//调用方法
object ret = _dynamicType.InvokeMember(methodName,BindingFlags.InvokeMethod, null, _dynamicObject, Arguments);
return ret;
}
}
具体调用
ComManage comManage = new ComManage()
{
CLSID = clsid,
Properties = new Dictionary
{
{"Host","ydtf-127"},
},
Arguments = new object[]
{
"测试单位",
"",
""
}
};
object ret = comManage.MethodInvoke("PKILogin");
4 .NET 4.0 调用
C#4.0新特性对.NET互操作的影响
说道C#的新版本对.NET互操作的影响就不得不先说一下C#4.0的新特性。
Dynamically Typed Objects.
Optional and Named Parameters.
Improved COM Interoperability.
Safe Co- and Contra-variance.
这其中第2、3条都和互操作有关系。第2点的可选参数和命名参数并不是什么新概念了。主要在于编译器的支持。像VB.NET早就支持可选参数了。这几年C#社区对这个特性的呼声太高了,看来终于起作用了。
4.1 可选参数
有很多COM方法都接受可选参数。在调用此类方法时,可以根据具体需要为可选参数传递指定的值,或者忽略此参数而使用该参数的默认值。在使用托管代码调用COM方法时,根据不同的.NET语言,调用的复杂度也有所差异。由于Visual Basic .NET本身就支持可选参数(Optional关键字),它能够以可选方式使用该参数。但如果使用C#,情况就会大为不同。由于C#不支持可选参数,因此就必须为方法中的每个参数传递值,比如可以为可选参数传递System.Type.Missing以设置该参数的默认值。由于必须为所有的可选参数传递值,因此使用C#调用带有可选参数的COM方法,就不如Visual Basic .NET方便和灵活。
使用过Office PIA的朋友,在操作word文档时一定遇到过下面的例子:
object fileName = "Test.docx";
object missing = System.Reflection.Missing.Value;
doc.SaveAs(ref fileName,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing);
为了调用SaveAs方法,你不得不为填写全部不必要的参数。这就是由于老的C#版本不支持可选参数的原因。
在C#4.0出现后,情况就大不一样了。比如上面的代码可以写成:
doc.SaveAs("Test.docx");
4.2对COM互操作的改进支持
新特性的第3点提到了特别针对COM互操作的改进。这包括:
1.在同一进程中host多个版本的CLR。这样可以为托管COM组建选择它所需(编译时)的运行时版本。
2. 不再必须使用PIA(Primary Interop Assembly)于COM组建交互。在过去,当你发布一个COM组建时,微软建议你随该组建发布一个PIA。这个附带的程序集PIA用来被托管应用程序客户端引用。在.NET Framework 4.0中PIA将被弱化。C#和VB编译器会判断你的程序具体使用了哪一部分COM API,并只把这部分包装成IA(互操作程序集),直接加入到你自己的应用程序集里面。
3. 重定义QueryInterface。你可以使用System.Runtime.InteropServices.ICustomQueryInterface接口自定义由托管代码实现的IUnknown::QueryInterface方法。应用程序可以用它返回特定的接口。
4.3 P/Invoke 调用
dynamic user32 = new DynamicDllImport("user32.dll",
callingConvention: CallingConvention.Winapi);
user32.MessageBox(0, "Hello World", "Platform Invoke Sample", 0);
DynamicDllImport是Mono发布的一个类,利用它我们可以直接调用WinAPI,这里调用了MessageBox函数。