锐英源软件
第一信赖

精通

英语

开源

擅长

开发

培训

胸怀四海 

第一信赖

当前位置:锐英源 / 视频介绍 / COM组件开发引导系列 / DBCOMMUL: COM Object with Multiple Interfaces
服务方向
人工智能数据处理
人工智能培训
kaldi数据准备
小语种语音识别
语音识别标注
语音识别系统
语音识别转文字
kaldi开发技术服务
软件开发
运动控制卡上位机
机械加工软件
软件开发培训
Java 安卓移动开发
VC++
C#软件
汇编和破解
驱动开发
联系方式
固话:0371-63888850
手机:138-0381-0136
Q Q:396806883
微信:ryysoft

1.1.9 DBCOMMUL: COM Object with Multiple Interfaces带有多接口的COM对象


1.1.9.1 Theory理论

The previous section introduced the QueryInteface function, which must be present on any COM interface. This function allows a client to ask the object for different pointers to itself that also point to different abstract base classes (also known as interfaces). The only interfaces we have implemented so far are IUnknown and IDB. 上节介绍了QueryInteface函数,这个函数在任何一个COM接口里都要有。这个函数允许客户端请求对象提供关于自己的其它方面的指针,这个新指针同样指向不同的虚基类(也叫做接口)。我们已经实现的接口现在只有IUnknown和IDB。

We will now look at a really useful way of implementing multiple interfaces. In the previous sample, IDB was just a superset of IUnknown. The IDB interface basically consists of three semantically related sections: 我们现在要看看一个实际上非常有用的多接口实现机制。在上个例子里,IDB只是IUnknown的一个超集。IDB接口基本上由3个语义相关的节构成:

  • Functions for accessing a table.存取数据库表的函数
  • Functions that allow creation and deletion of tables (that is, they manage the database).允许创建和删除数据库表(管理数据库)的函数
  • Functions that return information on the database and a table. 返回数据库和表的信息的函数

Suppose different clients have different ways of using our object. For example, some clients may just want to read and write to existing tables. Others may want to create and delete tables but not to read their contents. Having additional functions on the interface to the object is not a major overhead cost, and has the advantage of exposing all the complexity of an object to all its users: A programmer who accesses the content of a table gets to see all the functions for managing the database. In our case, the functions are not tremendously complex, but real objects could expose hundreds of different functions.假定不同的客户端有不同的方法来使用我们的对象。比如,一些客户端可能只想读和写已有表的信息。其它的想创建和删除表,但是不读写。在对象接口上保留多余的函数代价不是太大,且有一个优势来揭示出对象对所有客户支持的复杂性:存取表内容的程序员会看到管理表的函数。在我们所处情况下,函数并不是过于复杂,但是实际的对象可能导出上百个不同的函数。

To show the general technique behind grouping of member functions, we will break down our IDB interface into three new interfaces, each derived from IUnknown:为了展示隐藏在这些成组的成员函数后面的通用技术,我们会把IDB接口拆分为三个接口,每个都从IUnknown派生出来:

class IDBAccess : public IUnknown {            
public:            
// Interface for data access            
virtual HRESULT _stdcall Read(short nTable, short nRow, LPWSTR lpszData) =0;            
virtual HRESULT _stdcall Write(short nTable, short nRow, LPCWSTR           
lpszData) =0;          
};          
class IDBManage : public IUnknown {            
// Interface for database management            
public:            
virtual HRESULT _stdcall Create(short &nTable, LPCWSTR lpszName) =0;            
virtual HRESULT _stdcall Delete(short nTable) =0;          
};          
class IDBInfo : public IUnknown {            
// Interface for retrieval of information about the database.            
public:            
virtual HRESULT _stdcall GetNumTables(short &nNumTables) =0;            
virtual HRESULT _stdcall GetTableName(short nTable, LPWSTR lpszName) =0;            
virtual HRESULT _stdcall GetNumRows(short nTable, short &nRows) =0;          
};

These three abstract base classes define three different vtable layouts: IDBAccess has five entries in its vtable—the three IUnknown functions plus two actual functions. IDBManage also has five member functions and IDBInfo has six entries in its vtable: three for IUnknown and three functions of its own.这三个虚基类定义三个不同的虚函数表:IDBAccess有5个入口—三个IUnknown的加2个实际的函数。IDBManage同样有5个成员函数且IDBInfo有6个入口:3个IUnknown的和三个自己的。

A client should be able to ask our object for any of these three interfaces. We have to return a pointer to a vtable that contains only the appropriate function addresses. How can we accomplish this?客户端应该能够要求我们对象返回这三个接口中的任何一个。我们必须返回的虚函数表里要包含对应的函数地址。我们怎样实现这个呢?

The easiest way to achieve this is through multiple inheritance. We simply derive our CDB implementation class from all the base classes that we want to provide interfaces for: IDB, IDBAccess, IDBManage, and IDBInfo. We keep IDB for providing backwards compatibility for existing clients.实现这个目标的最简单的方法是多重继承。从所有这些类上派生CDB出来,包括IDB, IDBAccess, IDBManage, 和 IDBInfo。我们保留IDB是为了兼容已经有的客户端。

What do multiple abstract base classes mean from the perspective of C++? If I have a pointer to an object cast to one of its base classes, I can call the correct member functions through the abstract base class. C++ implements this through one vtable layout per class that the object points to in its instance data. If I have a pointer to the same object, but cast to another abstract base class, I am able to do the same—call the members through their vtable position. The cast pointer to the instance must also contain a pointer to the vtable in its instance data. 从C++的深度上看,多个虚基类意味着什么呢?如果我有一个对象的指针,转换到它的基类指针对象上,我能通过基类指针调用合适的成员函数。C++实现这个是通过每个类的虚函数表排列,这些类的实际对象要是COM对象指向到的。如果我有一个指向同样对象的指针,但转换它到其它虚基类上,我能够实现同样功能—通过虚函数表位置调用成员。转换到实例的指针必须同样包含有虚函数表的指针。

How does C++ handle multiple pointers to different base classes It provides multiple vtables, one for each base class, and multiple pointers within the object's instance data. When casting a pointer, the compiler simply adds the offset to the correct part of the object's instance data. In our case, the object derives from four abstract base classes, and thus has four vtable pointers at the beginning of its instance data.C++是怎样处理多个指向不同基类的指针的呢?它提供了多虚函数表,和类一一对应,在对象数据里有多个指针保存呢。当转换一个指针时,编译器只是添加合适偏移到指针上,进而指向在对象实例数据上。在我们的情况里,从四个抽象基类上派生的对象,这样在对象开头会有四个虚函数表。

For example, when casting occurs from CDB to IDB, the compiler adds 0; when casting occurs from CDB to IDBAccess, the compiler adds 4 to the pointer; when casting occurs from CDB to IDBManage, the compiler adds 8; and so forth.比如,当从CDB转到IDB时,编译器添加0;当从CDB转到IDBAccess时,编译器添加4;当从CDB转到IDBManage时,添加8.别的类似。

There is one dedicated vtable for each interface on CDB—four total for all instances of CDB. The compiler initializes these vtables with the correct function addresses. If two abstract base classes include the same function (such as Read in IDB and IDBAccess, or AddRef in all four interfaces), the compiler simply repeats the function address. One implementation of AddRef is called from an invocation of any base interface's AddRef.对于CDB,有一个为所有类实例服务的虚函数表—四个总的来服务于所有CDB的实例。编译器用正确的函数地址初始化这些虚函数表。如果2个抽象基类包含同名的函数(比如IDB和IDBAccess里的Read,或四个接口里都有的AddRef),编译器只是简单地重复函数地址。AddRef的一个实现是调用基接口的AddRef的引用。

This makes implementing multiple interfaces ridiculously simple—we just derive CDB from all four interfaces and expand QueryInterface to return the correctly cast pointer. C++ takes care of the rest.这让多接口实现起来非常简单—我们只是从四个接口派生出CDB且扩展出QueryInterface来返回正确的转换过的指针,C++实现了其它细节。

This section definitely contains more new ideas than new code, and really understanding what it does will help you a lot in understanding the flexibility and power of COM.本节里概念多,新代码少,真正理解实现机制会帮你理解COM的复杂和强大。
NoteOne of the major drawbacks of this approach to implementing multiple interfaces is that you can’t provide reference counting on an interface basis. More complex objects could need to load additional code or initialize additional data structures, when being asked for more complex functionality. With multiple inheritance, any call to Release goes to the same function. The object can not free the additional code/data, because it does not know which interface pointer was released. The big advantage of using multiple inheritance for this lies in its simplicity. All the dangers of normal C++ multiple inheritance, such as multiple common base classes, and so forth, do not really apply, because interfaces contain no data members.这个方法的一个缺点是你不能提供基于基接口的引用计数。当请求更多复杂的功能时,更多复杂的对象能够加载额外的代码或初始化额外的数据结构。用多继承, Release调用会使用同一个函数。对象不能释放额外的代码/数据,因为它不知道哪个接口释放了。多继承的优势就是简单。所有C++多继承的危险,比如多个通用基类,并不影响,因为接口不包含数据成员。

1.1.9.2 Practice实践

Inasmuch as we are not going to add a lot of code, but we will add three new interfaces, I will present a more elegant way of managing the interface IDs (in general, GUIDs). In the previous samples, the GUIDs were not defined in the Interface\dbsrv.h header file, because multiple "includes" of the header in a project would have resulted in multiple definitions of the GUIDs. There is a simple macro, defined through OLE2.H, that helps us provide the GUID in a header file: DEFINE_GUID. This macro expands in two different ways, depending on an INITGUID symbol. If this symbol is defined, it expands to a definition of the GUID. If it is not defined, it expands to a declaration of the GUID without initializing it.因为情况差不多了,我们不会再添加很多代码,但是我们会添加三个新的接口,我会用一个更优雅的方法来管理接口ID(通常的GUID)。在上个例子里,GUID没有在Interface\dbsrv.h里定义,因为头文件重复“包含”会导致重定义。有一个简单的宏,在OLE2.H里定义,这个宏能够帮助我们在头文件里提供出来GUID:DEFINE_GUID。这个宏以2个不同的方法来扩展,依赖于INITGUID标志。如果这个标志定义了,它扩展为定义GUID。如果没有定义,它扩展为声明GUID,而不用初始化它。

One source file must contain #define INITGUID before including DBSRV.H, in order to provide the definition of the symbol. This requires precompiled headers to be disabled for this source file, otherwise the compiler uses the precompiled header that includes the wrong macro expansion. 为了提供标志的定义,一个源文件必须在包含DBSRV.H前包含#define INITGUID。这需要预编译头文件对此源文件禁止,否则编译器使用预编译头文件会导致包含错误的宏扩展。

1.1.9.3 Changes: Step by Step逐步修改

Keep a copy of the unmodified client to test the backwards compatibility of the new object! 保留一个未修改的客户端来检测新COM对象的向下兼容性

  • Add definitions for IDBAccess, IDBManage, and IDBInfo to DBSRV.H.在DBSRV.H里添加IDBAccess, IDBManage, 和 IDBInfo类的定义。
  • Declare IIDs using the DEFINE_GUID macro: Generate new IIDs with GUIDGEN/UUIDGEN or use one of your unused, pre-generated GUIDs.使用DEFINE_GUID宏来声明IID:用GUIDGEN/UUIDGEN来生成新的IID或使用一些没用过的以前生成的GUID。
  • Derive CDB multiply from IDB, IDBAccess, IDBManage, and IDBInfo (in Object\dbsrvimp.h).从IDB, IDBAccess, IDBManage, 和 IDBInfo派生出CDB类(在Object\dbsrvimp.h里)。
class CDB : 
public IDB, 
public IDBAccess, 
public IDBManage, 
public IDBInfo {          
(...)          
};
  • Change CDB::QueryInterface to allow for the new interfaces.修改CDB::QueryInterface来支持新接口。
HRESULT CDB::QueryInterface(REFIID riid, void** ppObject) {          
if (riid==IID_IUnknown || riid==IID_IDB) {          
*ppObject=(IDB*) this;          
}                    
else if (riid==IID_IDBAccess) {                     
*ppObject=(IDBAccess*) this;                     
}                     
else if (riid==IID_IDBManage) {                     
*ppObject=(IDBManage*) this;                     
}                     
else if (riid==IID_IDBInfo) {                     
*ppObject=(IDBInfo*) this;                     
}
else {          
return E_NOINTERFACE;          
}          
AddRef(); 
return NO_ERROR;      
}
  • Remove the old definition of the CLSID and the IID in object\dbsrv.cpp and object\dbsrvfact.cpp. Create a file called Interface\guids.cpp that #defines INITGUID, #includes ole2.h, and then #includes dbsrv.h. Add interface\guids.cpp to the project and deactivate precompiled headers for it.删除在文件object\dbsrv.cpp 和object\dbsrvfact.cpp里的旧的CLSID和IID的定义。创建命名为Interface\guids.cpp的文件,它有#defines INITGUID语句,#includes ole2.h语句和 #includes dbsrv.h语句。添加interface\guids.cpp到工程里且对这个文件不激活预编译头文件的使用。
  • In the client, call CreateInstance with IID_IUnknown and change m_pDB to IUnknown*.在客户端里,带IID_IUnknown调用CreateInstance且修改m_pDB为IUnknown*类型。
  • In the client, before calling Create, Read, and Write, use QueryInterface on m_pDB to obtain the appropriate interface. Release the obtained interface pointer after using it.在客户端里,在调用Create,Read和Write前,用m_pDB使用QueryInterface来获取适当的接口。在使用过后用Release来释放获取的接口。
  • Add Interface\Guids.cpp to the project and deactivate precompiled headers for it. Remove the declaration of CLSID and IIDs from client\dbdoc.cpp 添加Interface\Guids.cpp到工程里,且关闭预编译头文件对此文件的参与。删除client\dbdoc.cpp里的CLSID和IID的声明。

Test the new object: Register it using regsrv32.exe and run the client. Also, try running the client that used IDB (from the DBCOM sample). Since we still support this interface, this client continues to work.测试新COM对象:用regsrv32.exe来注册,运行客户端。同样尝试运行使用IDB(从DBCOM例子里)的客户端。因为我们依然支持这个接口,这个客户端也能工作。

QueryInterface provides a great mechanism for maintaining backwards compatibility between versions of an object. It also provides a method for very flexible version checking. The client and the server do not check an abstract "version number" where both have to agree somehow what a specific version means in terms of functionality and interfaces. A client can check for each specific feature by asking for an interface through QueryInterface. A client could also ask first for a more sophisticated new interface, and if the object does not provide it, the client can ask for an "older" interface and provide a workaround. Using this mechanism you can provide interoperability between both a new client with an old server and an old client with a new server. QueryInterface提供了一个强大的机制来维护向下兼容性。它同样提供一个方法来达到非常灵活的版本检查。客户端和服务器端不检查一个双方约定的抽象“版本号”,约定的意思是指指定的版本号和功能和接口有关联。客户端能检查每个指定的接口,这是通过用QueryInterface请求访问一个接口来实现。客户端同样能够首先检查一个更成熟的新接口,且如果COM对象不能提供它,客户端能够请求“旧的”,能让工作走通。使用这个机制,你能提供对新旧客户端的协调性。

This is why OLE 2.0 is not OLE 2.0 anymore. It is just OLE. New features do not require a totally new version, they are just added on top of the old ones and each user queries for the features it needs.这就是OLE2.0不再只是OLE2.0的原因。它只是OLE。新技术并不需要一个全新的版本,他们只是在肩膀上再站的更高且每个用户请求它需要的技术。

友情链接
版权所有 Copyright(c)2004-2021 锐英源软件
公司注册号:410105000449586 豫ICP备08007559号 最佳分辨率 1024*768
地址:郑州市金水区郑州大学北校区院(文化路97号院)内