运行环境:visual studio.net beta2, vc7, c#
参考资料:msdn
级别:入门级
一、前言
com组件对象与.net类对象是完全不同的,但为了使com客户程序象调用com组件一样调用.net对象,使.net程序
象使用.net对象一样使用com组件,ms使用了wrapper技术。本文详细介绍了两种不同的wrapper技术,并给出了
简单的代码实例。
二、com wrapper简介
传统的com对象与.net框架对象模型有以下几点不同:
(1)、com对象的客户必须自己管理com对象的生存期,而.net对象的生存期由clr(common language runtime)来管
理,即通过gc(garbage collection)机制自动回收。
(2)、com对象的客户通过调用queryinterface查询com对象是否支持某个接口并得到其接口指针,而.net对象的客
户使用reflection(system.reflection.*)来获得对象功能的描述,包括方法属性等。
(3)、com对象的客户通过指针引用com对象,对象在内存中的位置是不变的,而.net对象在内存中的驻留由.net框
架执行环境(execution environment)来管理,对象在内存中的位置是可变的,比如出于优化性能的考虑,同时
会更新所有对对象的引用。这一点也是以clr中不使用指针为前提的。
为了实现传统的com程序与.net程序之间的相互调用,.net提供了包装类rcw(runtime callable wrapper)和
ccw(com callable wrapper)。每当一个.net客户程序调用一个com对象的方法时就会创建一个rcw对象,每当一个
com客户程序调用一个.net对象的方法时就会创建一个ccw对象。
具体示意图如图1所示:

图1 com wrapper overview
三、.net中调用com组件
1、rcw(runtime callable wrapper)简介
其示意图如图2所示:

图2 accessing com objects through the runtime callable wrapper
rcw的主要功能:
(1)rcw实际上是runtime生成的一个.net类,它包装了com组件的方法,并内部实现对com组件的调用。
(2)列集(marshal).net客户与com对象之间的调用,列集的对象包括方法的参数返回值等,比如c#中的string与
com中的bstr之间的转换。
(3)clr为每个com对象创建一个rcw,与对象上的引用数无关,就是说每个com对象有且只会有一个rcw对象。
(4)rcw中包含了com对象的接口指针,并管理com对象的引用计数。rcw自身的释放通过gc机制管理。
2、实例演示
(1)使用vc7/atl创建一个最简单的com对象。组件类名叫atlcomserver,实现的接口名叫iatlcomserver,库名叫
atlserver。添加一属性name,并实现get/set函数。其idl如下所示:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(77506e08-d9fb-4f45-85e0-376f5187af21),
dual,
nonextensible,
helpstring("iatlcomserver interface"),
pointer_default(unique)
]
interface iatlcomserver : idispatch{
[propget, id(1), helpstring("property name")] hresult name([out, retval] bstr* pval);
[propput, id(1), helpstring("property name")] hresult name([in] bstr newval);
};
[
uuid(9136eee6-ecee-4237-90b6-c38275ef2d82),
version(1.0),
helpstring("atlserver 1.0 type library")
]
library atlserverlib
{
importlib("stdole2.tlb");
[
uuid(0e733e15-2349-4868-8f86-a2b7ff509493),
helpstring("atlcomserver class")
]
coclass atlcomserver
{
[default] interface iatlcomserver;
};
};
(2)创建一个最简单的c# console程序。执行菜单project/add reference命令,在com属性页中选中刚才创建的
atlserver 1.0 type library并添加,系统会提示是否添加一个wrapper,选择是,然后会自动在c#程序的
bin目录下生成一个文件interop.atlserverlib_1_0.dll,这个就是atlserver的rcw。另外使用命令行命令
tlbimp atlserver.tlb有同样的效果。
(3)在程序中添加调用altserver的代码,如下所示:
using system;
using atlserverlib; //通过namespace来引用库,在wrapper(即interop.atlserverlib_1_0.dll)中定义
namespace csharpclient
{
class class1
{
static void main(string[] args)
{
atlcomserver server = new atlcomserver();
server.name = "chang ming";
console.writeline("hello, my names is " + server.name);
}
}
}
从上面可以看到,atlserverlib.atlcomserver就代表了com组件atlcomserver。在传统的com客户中通过接口
iatlcomserver来调用,而在.net中只是把它当作了一个普通的.net类。因为实际上调用的是wrapper中的类,
而不是真正的com对象。
下载rcw的示例代码(23kb)
四、com程序中调用.net对象
1、ccw(com callable wrapper)简介
其示意图如图3所示:

图3 accessing .net objects through com callable wrapper
ccw的主要功能:
(1)ccw实际上是runtime生成的一个com组件,它在注册表注册,有clsid和iid,实现了接口,内部包含了对
.net对象的调用。
(2)列集(marshal).net对象与com客户之间的调用。
(3)每个.net对象只有一个ccw,多个com客户调用同一个ccw。
(4)com客户以指针的方式调用ccw,所以ccw分配在non-collected堆上,不受runtime管理。而.net对象则分配
在garbage-collected堆上,受runtime管理,享受clr的种种好处。
(5)ccw实际上是com组件,所以它遵循引用计数规则。当它的引用计数为0时,会释放它对它管理的.net对象的
引用,并释放自己的内存空间。当.net对象上引用计数为0时,则会被gc回收。
.net中受控类型(manages types)如class、interface、struct和enum都可以无缝的与com类型相结合,但是要
遵循以下规则:
(1)受控类型必须是public型。只有public型的类型才会被输出到类型库中。
(2)只有public型的methods、properties、fields和events才会被输出到类型库中,才会被com客户看见。
(3)受控类型必须有一个公用的缺省构造函数。这是因为com组件要求必须有缺省构造函数。
(4)强烈推荐.net类中显式地实现接口。如果一个.net类没有显式地实现一个接口,com interop会自动为其生
成一个接口,该接口包含了这个.net类及其父类的所有公有成员。这个被自动生成的接口被称为"class interface"。
但是ms强烈推荐使用显式的接口定义,原因在下面阐述。
2、实例演示一(不显示定义接口)
(1)创建一个最简单的c# console工程,其程序如下所示:
using system;
using system.runtime.interopservices;
namespace csharpserver
{
//缺省的是classinterfacetype.autodispatch,该方式下只生成dispatch接口
//只能被使用script、vb等late binding方式的com客户使用
[classinterfaceattribute(classinterfacetype.autodual)]
public class sharpobject
{
private string m_strname;
public sharpobject(){}
public string name //property: name, get/set
{
get { return m_strname; }
set { m_strname = value; }
}
}
}
(2)在工程的属性中设置register for com interop为true。这样编译后就会生成csharpserver.tlb文件,并且
自动将其注册。命令行命令regasm有同样的效果。注册表内容如下:
[hkey_classes_root\clsid\{88994e22-e99f-320b-908c-96e32b7bfe56}]
@="csharpserver.sharpobject"
[\inprocserver32]
@="c:\\winnt\\system32\\mscoree.dll"
"threadingmodel"="both"
"class"="csharpserver.sharpobject"
"assembly"="csharpserver, version=1.0.583.39183, culture=neutral, publickeytoken=null"
"runtimeversion"="v1.0.2914"
"codebase"="file:///e:/cm/net/c%23/exer/csharpserver/bin/debug/csharpserver.dll"
[\progid]
@="csharpserver.sharpobject"
csharpserver.tlb文件中包含了组件的类型库信息,包括clsid、iid、接口定义等。而组件的真正实现,对.net
对象的调用则是由通用语言运行时库mscoree.dll完成的。可以说mscoree.dll和csharpserver.tlb加起来就是runtime为csharpserver这个.net类生成的ccw。
(3)写一个简单的vbscript程序test.vbs,如下所示:
dim obj
set obj = createobject("csharpserver.sharpobject")
obj.name = "chang ming"
msgbox "my name is " & obj.name
双击该文件,成功运行。
(4)创建一个最简单的mfc对话框工程,加入以下代码:
//这里应该用raw_interfaces_only,因为sharpobject缺省的从objec
//如果不加这个选项的话,也要为object的公用函数和属性生成包装函数,
//而object::gettype返回type型,而没有为类type生成包装接口,所以编译时会出错
#import "..\csharpserver\bin\debug\csharpserver.tlb" raw_interfaces_only no_namespace named_guids
…
{
coinitialize(null);
//方法一
//因为使用了raw_interfaces_only,所以没有生成属性name的包装函数getname,putname
_sharpobjectptr psharpobject(__uuidof(sharpobject));
psharpobject->put_name(_bstr_t("chang ming"));
bstr strname;
psharpobject->get_name(&strname);
afxmessagebox("my name is " + _bstr_t(strname));
//方法二
/* _sharpobject *psharpobject = null;
hresult hr = cocreateinstance(clsid_sharpobject,
null,
clsctx_inproc_server,
iid__sharpobject,
(void**)&psharpobject);
if (succeeded(hr))
{
psharpobject->put_name(_bstr_t("chang ming"));
bstr strname;
psharpobject->get_name(&strname);
afxmessagebox("my name is " + _bstr_t(strname));
psharpobject->release();
}
else
{
afxmessagebox("error");
}
*/
couninitialize();
}
自动生成的class interface中,接口名是_+类名,即_sharpobject。除此之外,使用方式与调用一般的com对
象完全一样。
(5)使用class interface的缺点在于.net类的变化会影响到com客户。具体而言,对于使用script、vb等late binding
方式的语言如test.vbs,net类的变化对其没有影响。而对于early binding的客户,因为dispid与其在.net类中
的位置相关,所以.net类的变化很有可能会改变成员的dispid,从而会影响到客户程序,客户程序需要重新编译。
对于通过指针直接调用的c++客户程序,每次.net的重新编译都会导致其重新编译,因为class interface的iid
每次都是随机生成的!所以ms强烈要求不要使用这种方式,class interface不能算是一个真正的接口,它总是
不断的改变,这违背了接口的精神,违背了com的精神。
3、实例演示二(显示定义接口)
(1)创建一个最简单的c# console工程,其程序如下所示:
using system;
using system.runtime.interopservices;
namespace csharpserver2
{
//如果不指定guid,每次都会随机生成iid
[guid("539448de-9f3b-4781-a1f6-f3c852091fc9")]
public interface isharpobject2
{
string name //property: name, get/set
{
get;
set;
}
void test();
}
//如果不指定guid,每次都会随机生成clsid
[guid("f5a31aab-faa9-47cc-9a73-e35606114ce8")]
public class sharpobject2 : isharpobject2
{
private string m_strname;
public sharpobject2(){}
public string name //property: name, get/set
{
get { return m_strname; }
set { m_strname = value; }
}
public void test(){}
}
}
(2)在工程的属性中设置register for com interop为true。这样编译后就会生成csharpserver2.tlb文件,并
且自动将其注册。注册表内容如下:
[hkey_classes_root\clsid\{f5a31aab-faa9-47cc-9a73-e35606114ce8}]
@="csharpserver2.sharpobject2"
[\inprocserver32]
@="c:\\winnt\\system32\\mscoree.dll"
"threadingmodel"="both"
"class"="csharpserver2.sharpobject2"
"assembly"="csharpserver2, version=1.0.583.38696, culture=neutral, publickeytoken=null"
"runtimeversion"="v1.0.2914"
"codebase"="file:///e:/cm/net/c%23/exer/csharpserver2/bin/debug/csharpserver2.dll"
[\progid]
@="csharpserver2.sharpobject2"
(3)创建一个最简单的mfc对话框工程,加入以下代码:
//这里不用raw_interfaces_only,因为sharpobject2只从接口isharpobject2继承
//而isharpobject2没有父类,所以不会有sharpobject那样的编译错误
#import "..\csharpserver2\bin\debug\csharpserver2.tlb" no_namespace named_guids
…
{
coinitialize(null);
//方法一
isharpobject2ptr psharpobject2(__uuidof(sharpobject2));
psharpobject2->putname("chang ming");
afxmessagebox("my name is " + psharpobject2->getname());
//方法二
/* isharpobject2 *psharpobject2 = null;
hresult hr = cocreateinstance(clsid_sharpobject2,
null,
clsctx_inproc_server,
iid_isharpobject2,
(void**)&psharpobject2);
if (succeeded(hr))
{
psharpobject2->putname("chang ming");
afxmessagebox("my name is " + psharpobject2->getname());
psharpobject2->release();
}
else
{
afxmessagebox("error");
}
*/
couninitialize();
}
只有接口isharpobject2保持不变,就不会影响到com客户程序。
下载ccw的示例代码(50kb)
