COM Clients and Servers

在COM的术语中,提供接口实现的的叫做COM服务端;而使用接口的叫做COM客户端。COM服务端又可以分为同进程(in-process )和异进程(out-of-process)两种。同进程的COM服务在DLL中实现,异进程的COM服务在EXE中实现。异进程的COM服务不止可以在本机,可以通过网络在远程提供服务。即便是同进程的COM服务,也可以通过转接(surrogate)的方式在EXE 中运行,从而对远程提供服务。查看DLL Surrogates

COM客户端在使用接口的时候,并不需要直到接口的服务端在哪边。就像是在超市买包薯片,并不需要直到薯片是在哪里生产的一样。

Getting a Pointer to an Object

几种获得COM对象指针的方式:

  • Call a COM library function that creates an object of a predetermined type—that is, the function will return a pointer to only one specific interface for a specific object class.
  • Call a COM library function that can create an object based on a class identifier (CLSID) and that returns any type of interface pointer requested. 阅览Creating an Object Through a Class Object
  • Call a method of some interface that creates another object (or connects to an existing one) and returns an interface pointer on that separate object.
  • Implement an object with an interface through which other objects pass their interface pointer to the client directly. 比如 OLE compound document container and server双向通信的例子。

Creating an Object Through a Class Object

COM服务端是通过一个COM类实现的。一个COM类可以实现多个接口。和C++中的类不同,COM类不是一个类型,支持对COM实现的一个称呼。COM类通过CLSID标识(128位的GUID)。客户端通过亲贵CLSID来激活一个COM类,从而实例化其对象,为客户端提供服务。

通过注册表COM把CLSID和COM类所在的DLL或者EXE关联在一起,这样客户端不需要知道COM类所在文件的位置。在分布式系统中,COM可以通过网络提供服务,这时候COM提供注册服务,远程的COM可以注册到客户端所在的机器上,或者COM客户端提供COM类所在服务器的地址。

However, for cases where it is desired, COM has replaced a previously reserved parameter of CoGetClassObject with a COSERVERINFO structure, which allows a client to specify the location of a server. Another important value in the CoGetClassObject function is the CLSCTX enumeration, which specifies whether the expected object is to be run in-process, out-of-process local, or out-of-process remote. Taken together, these two values and the values in the registry determine how and where the object is to be run.

远端激活需要考虑一些安全信息,参考Security in COM

最基本的实例化COM类的方式是通过类对象,这是一些提供IClassFactory接口的中间对象,可以通过 CreateInstance方法来创建示例。具体参考Implementing IClassFactory

当客户端需要激活一个COM类的实例,可以通过调用CoGetClassObject

另外一种创建COM对象的方式是使用Class Monikers

对于创建COM对象:

In previous releases of COM, the primary mechanism used to create an object instance was the CoCreateInstance function. This function encapsulates the process of creating a class object, using that to create a new instance and releasing the class object. Another function of this kind is the more specific OleCreate, the OLE compound document helper that creates a class object and retrieves a pointer to a requested object.

为了支持分布式系统,COM引入了其他几种方法:

  • Class monikers and IClassActivator
  • CoCreateInstanceEx,可以用于创建一个远程的未初始化对象,并且一次返回多个接口指针
  • CoGetInstanceFromFile,CoCreateInstanceEx的包装,提供从文件初始化功能
  • CoGetInstanceFromIStorage,CoCreateInstanceEx的包装,提供从存储初始化功能

COM Server Responsibilities

作为一个COM服务端,其义务是给客户端提供一个接口指针,为了做到这一点。服务端必须要确保自己能够被加载,并且能够创建客户端所需要的接口指针。

为了能够返回客户端所需要的接口指针,服务端必须实现IClassFactory或者IClassFactory2。为了能够被加载,服务端必须将自己的CLSID注册到自己所在机器的注册表。如果需要被远端的客户端所使用,那么还需要把自己的地址注册到远程机器。如果是作为同进程服务,那么必须在DLL接口上导出相应的函数供客户端进程来初始化它。

当客户端使用CLSID 来请求创建一个对象实例的时候,第一步是创建一个这个COM类的类对象。这是一个是实现了IClassFactory的对象。在COM提供的几个创建对象的函数中,都使用到了类对象以及IClassFactory接口。

IClassFactory包含两个方法:

  • CreateInstance:创建并返回一个未经初始化的实例并返回其指针
  • LockServer:增加类对象的引用计数,防止提早释放此类对象

调用IClassFactory的CreateInstance接口的时候,会进行授权检查,如果本地没有授权,那么实例化会失败。 IClassFactory2 继承了IClassFactory,并扩展了授权检查功能,可以在本地没有授权的情况,通过 IClassFactory2提供的额外接口来提供授权信息,摘抄如下:

  • The GetLicInfo method fills a LICINFO structure with information describing the licensing behavior of the class factory. For example, the class factory can provide license keys for run-time licensing if the fRunTimeKeyAvail member is TRUE.
  • The RequestLicKey method provides a license key for the component. A machine license must be available when the client calls this method.
  • The CreateInstanceLic method creates an instance of the licensed component if the license key parameter (BSTRÂ bstrKey) is valid.

主要的使用场景,一个开发者买了第三方的COM库,用来开发一个应用程序。当开发者把这个应用程序发布给用户的时候,需要把这个COM库带上。但用户的机子上并没有COM库的授权,所以开发者需要使用IClassFactory2接口的方法,用来附带上授权信息。

具体使用方法摘抄如下:

First, you need a separate development tool that is also a client of the licensed component. The purpose of this tool is to obtain the run-time license key and save it in your client application. This tool runs only on a machine that possesses a machine license for the component. The tool calls the GetLicInfo and RequestLicKey methods to obtain the run-time license key and then saves the license key in your client application. For example, the development tool could create a header (.h) file containing the BSTR license key, and then you would include that .h file in your client application.

To instantiate the component within your client application, first try to instantiate the object directly with IClassFactory::CreateInstance. If CreateInstance succeeds, the second machine is licensed for the component and objects can be created at will. If CreateInstancefails with the return code CLASS_E_NOTLICENSED, the only way to create the object is to pass the run-time key to the CreateInstanceLic method. CreateInstanceLic verifies the key and creates the object if the key is valid.

Registering COM Servers

实现了类对象以及IClassFactory之后,就可以把COM的CLSID注册到注册表了。注册这个动作,不仅可以在COM加载前做,也可以在COM被加载之后做。

通常情况下,COM的注册时在安装COM组件到一台机器的时候做的,需要指定CLSID和AppID 。注册的时候COM可以被指定成同进程,异进程,异进程远程。AppID 下有两个子项: RemoteServerName 和ActivateAtStorage,适用于异进程远程,让本地客户端能够在不知道该对象所在机器的情况下根据注册表信息激活该对象的实例。参考Locating a Remote ObjectInstance Creation Helper Functions.

COM服务端也可以以系统服务的方式运行,甚至在另一个用户权限下运行,参考Installing as a Service Application。如果服务端不是以服务端或者其他用户权限运行,那么成为 “activate as activator"服务端。在这种情况下,客户端的上下文(安全,以及window station/desktop )必须和服务端一致。如果一个客户端激活的是远程服务端,那么其window station/desktop为NULL。这是客户端的安全信息会受到检查。客户端的window station/desktop在调用CoInitialize和CoInitializeEx之后不能发生改变。

如果是同进程的服务端,那么可以通过 CoGetClassObject 来激活。在这个过程中,COM会调用 DllGetClassObject来获取具体的类对象。DllGetClassObject是同进程服务端必须实现的一个函数。

在EXE中的COM对象可以在被请求的时候调用CoRegisterClassObject 来注册自己的类表(与运行对象表不同)中的COM类的CLSID 。在类表中注册后,可以让SCM帮助判断COM服务端是否已经运行,否要需要再次加载。只有COM服务端不在类表之中时,SCM才会以CLSID查询注册表来获得相应信息。在调用CoRegisterClassObject 之后调用CoGetClassObject 就能获得类对象接口的指针。当下列条件都满足:

  • There are no existing instances of the object definition.
  • There are no locks on the class object.
  • The application providing services to the class object is not under user control (not visible to the user on the display).

当一个COM服务端创建了一个对象之后,会为这个对象创建一个moniker ,然后把这个对象放置到ROT中,通过调用IRunningObjectTable::Register。当服务端使用CreateFileMoniker来创建这个Moniker的时候,必须使用基于盘符的路径,而不是UNC格式的路径。

stopped at Self-Registration

Out-of-Process Server Implementation Helpers

Persistent Object State

COM对象可以根据客户端的请求来将自己的状态持久化,也就是序列化到文件、结构化存储或者内存里面。客户端可以决定序列化的位置,但是不能决定序列化的格式。遵循此规则的COM对象成为持久化对象。

一个持久化对象实现一至多个持久化对象接口,所有的持久化对象接口都是从IPersist派生出来的。目前COM提供的持久化对象接口包括:

  • IPersistStream
  • IPersistStreamInit
  • IPersistStorage
  • IPersistFile
  • IPersistMoniker
  • IPersistMemory
  • IPersistPropertyBag

不同类别的COM通常支持不同的持久化对象接口:

  • Monikers: IPersistStream
  • OLE embeddable objects: IPersistStorage, IPersistFile
  • ActiveX controls: IPersistStreamInit, IPersistStorage, IPersistMemory, IPersistPropertyBag, IPersistMoniker
  • ActiveX document objects: IPersistStorage, IPersistFile

有一些接口 IPersistStreamInit, IPersistStorage, IPersistMemory, 以及IPersistPropertyBag允许创建对象的时候把其状态置为未初始化。原理是因为初始化操作可能是一个耗时比较久的操作,如果能允许必要的时候再初始化,可以提高一定的性能。

如果客户端想知道COM对象的类型信息,一种是通过注册表根据CLSID查询,另一种是通过接口 IProvideClassInfo或者 IProvideClassInfo2的GetClassInfo查询。当然,COM服务端必须支持这两个接口才行。GetClassInfo返回ITypeInfo,指向COM对象的coclass信息。通过ITypeInfo,客户端可以检查该COM对象所有的提供的以及使用到的接口。

Inter-Object Communication

既然COM是通过指针来提供服务的。指针这种东西只在同一进程内有效。所以一旦跨了进程,就必须使用RPC机制。但是COM的客户端以及服务端一般不需要察觉RPC的存在,客户端有代理(Proxy),服务端有驻桩(stub),可以办理RPC,隐藏其细节。而对于跨进程的接口,其指针则是由代理或者驻桩提供的。

跨进程RPC还需要把调用信息以及返回结果编组。COM提供standard marshaling机制,一般情况下能满足绝大部分需求。COM对象可以通过IMarshal接口来使用自定义的编组。一个COM对象还可以再同进程以及异进程的COM调用使用不同的编组方式,而完全对客户端以及服务端不可见。

做到这一点,需要在虚函数表上做手脚。接口指针指向的是一个虚函数表,通过虚函数表再定位到具体的函数实现。对于代理或者驻桩的情况,接口指针指向的虚函数表里的内容其实是由代理或者驻桩提供的。

对于standard marshaling机制:

  • In the case of most COM interfaces, the proxies and stubs for standard marshaling are in-process component objects which are loaded from a systemwide DLL provided by COM in Ole32.dll.
  • In the case of custom interfaces, the proxies and stubs for standard marshaling are generated by the interface designer, typically with MIDL. These proxies and stubs are statically configured in the registry, so any potential client can use the custom interface across process boundaries. These proxies and stubs are loaded from a DLL that is located via the system registry, using the interface ID (IID) for the custom interface they marshal.
  • An alternative to using MIDL to generate proxies and stubs for custom interfaces, a type library can be generated instead and the system provided, type-library–driven marshaling engine will marshal the interface.

对于COM对象的任意一个接口,可以选择使用COM提供的standard marshaling或者自定义编组。一旦选择,再COM对象的生存周期以内无法修改。COM对象的不同接口可以选择不同的编组方式。

COM代理的构成可以分为两部分,一部分是代理经办(Proxy Manager),另一类是接口委托(interface proxy)。代理经办是由系统提供的,用来管理接口委托。接口委托本身是COM对象,可以有多个,针对使用到的不同的接口。接口委托需要实现一个私有的IRpcProxyBuffer来处理内部沟通。每个接口委托的实现可以再不同的DLL中,根据需要来加载。接口委托使用 IRpcChannelBuffer接口把编组后的接口放置到channel上传送。更多请看Structure of the Proxy

COM驻桩的结构跟代理有点类似,不同点如下:

  • The most important difference is that the stub represents the client in the object’s address space.
  • The stub is not implemented as an aggregate object because there is no requirement that the client be viewed as a single unit; each piece in the stub is a separate component.
  • The interface stubs are private rather than public.
  • The interface stubs implement IRpcStubBuffer, not IRpcProxyBuffer.
  • Instead of packaging parameters to be marshaled, the stub unpackages them after they have been marshaled and then packages the reply.

关于驻桩的结构,参考Structure of the Stub

COM的RPC是基于 Open Software Foundation (OSF) Distributed Computing Environment (DCE) RPC。微软把自己的实习成为Microsoft RPC,具有以下特性:

You can configure RPC to use one or more transports, one or more name services, and one or more security servers. The interfaces to those providers are handled by RPC. Because Microsoft RPC is designed to work with multiple providers, you can choose the providers that work best for your network. The transport is responsible for transmitting the data across the network. The name service takes an object name, such as a moniker, and finds its location on the network. The security server offers applications the option of denying access to specific users and/or groups. See Interface Design Rules for more detailed information about application security.

微软还定义了自己的接口描述语言MIDL以及相应的编译器来生成代码。查看Building and Registering a Proxy DLL

其他参考

(本篇完)