锐英源软件
第一信赖

精通

英语

开源

擅长

开发

培训

胸怀四海 

第一信赖

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

1.1.7 DBAlmostCOM: C++ Object in a DLL, Loaded Through COM

     以COM方式加载的在DLL里的C++对象

The previous sample provided a very flexible mechanism for object packaging: Only one entry point had to be declared in the DLL, and all other interchange was done through pointers to pure abstract base classes.

Next, we will make some minor changes that will make this object almost COM-compliant.上个例子提供了一个非常复杂的对象打包机制:在DLL里只有一个入口点必须声明出来,且其它交互通过纯抽象基类的指针来进行。随后我们会进行小的修改让对象几乎和COM兼容。

1.1.7.1 Single Standard Entry Point for Object Creation为对象创建服务的单个标准入口点

Suppose you want to pack multiple objects into a single DLL. Basically, you have two options: 假定你想打包多个对象到一个单独的DLL里。基本上,你有2个选择:

Provide one entry point function for each class you want to export.为想导出的每个类提供一一对应的入口函数。

Pass an additional parameter to a standard entry point, indicating which class you want. 向标准入口传递额外的参数,指示你需要哪个类

The second approach seems to be more appropriate for a generic component model, because it provides one central entry point for creating all the objects you want. COM does exactly this, and requires all objects implemented in DLLs (also called in-process servers) to export the entry point through the DllGetClassObject function.第二个方法好象更适合于通用组件模式,因为它提供了一个中央入口来创建所有你想要的对象。COM确实也是这样,且要求所有在DLL里实现的对象(也称作是进程内服务)来通过DllGetClassObject函数来导出入口点。

This function receives a parameter of type CLSID that specifies which class the caller wants to access. The CLSID is nothing but a relatively big number (16 bytes—yes, bytes, not bits—in case many people want to create objects). 这个函数接收类型为CLSID的参数,这个参数指明调用者想访问哪个类。CLSID只是一个相对大的数字(16字节,提供的空间来容纳很多人想要创建的类)。

The DLL checks the number of the class that is requested, and if it provides that class, it returns a pointer to an object that implements the creation method for the actual object. This is the architecture we used already for both of our DLL samples. Introducing this intermediate object allows for flexible instantiation of objects. There is another technical reason for this approach that is related to objects implemented in executables, which we will discuss later.DLL检查请求类的数字,如果能够提供出来类,它返回出来一个对象的指针,这个对象实现了某些实际对象创建上的方法。这是我们已经用于我们DLL例子里的架构。引入这个中间对象允许一个灵活的对象实例化。这个方法还有其它另外一个技术原因,这个技术是关于在可执行文件里实现的对象,这会在随后讨论。

Microsoft provides a tool for generating CLSIDs that are guaranteed to be globally unique. This is done (a little simplified) by using the worldwide unique ID of the network card, combined with the current date and time. This combination fits into 16 bytes, plus there is plenty of space left over for a simple counter that can give you an (almost) arbitrarily large range of contiguous CLSIDs. Because these numbers are used for many things — not just as class identifiers—they are called globally unique identifiers (GUIDs).微软提供了一个工具来生成CLSID,这个工具会保证全球唯一性。这是通过(简单来说)使用全球唯一的网卡ID和当前时间和日期结合来生成的。这个结合写入到16个字节里,再加上有充足的空间为简单的计数预留,进而让你(几乎)有绝对空间来让多个CLSID提供出来。因为这些数字用于很多事情—不只是类标识—他们称为全球唯一标识(GUIDs)。

Here is our standard entry-point function in completed form:完成形式如下的标准入口函数:

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID * ppObject);

STDAPI is just a macro that is replaced (by the compiler) with HRESULT and some calling convention.STDAPI只是宏,编译器会替换它为HRESULT和一些调用约定。

We find our CLSID (here declared as a reference to a CLSID), and our "pointer to a pointer" that receives the pointer to the object's instance. Note that this pointer is declared as "void," because we will return arbitrary base class pointers through this parameter. 我们发现我们的CLSID(这里声明一个引用来代表CLSID),且我们的“二维指针”会接收对象实例的指针。注意这个指针声明为“void”,因为我们会通过这个参数返回任意的基类指针。

What about the additional parameter, riid, that looks like another of those GUIDs IID stands for interface ID. With the riid parameter, the caller indicates what kind of pointer it expects to be returned through ppObject. The object returned is the class factory object, and the interface ID assures the caller that it "understands" the pointer. The object will check that the caller actually requests our interface IDBSrvFactory, for which we will designate a unique IID.riid参数看起来象另外一个GUID?IID代表了接口ID。使用riid参数,调用者指示哪个类型的指针需要从ppObject里返回出来。返回的对象是类工厂对象,接口ID确保调用者“理解”指针。对象将检查调用者实际请求的是自己的接口IDBSrvFactory,这个接口会指派一个唯一的ID。

int **ppI;

int *pi;

*ppi=pi;

1.1.7.2 Standard Object Creation API标准的对象创建API

Once this standard entry point is exported, the COM libraries can manage our DLL as an object, even though it is not yet a real COM object. We can register our object—its CLSID and the path to the DLL—and have COM worry about loading the DLL. Before we used implicit DLL linking in our client—the linker put in some code to load the specific DLL we had compiled with, and the name of the DLL was hard-coded into the client's executable. Now we will just tell the COM libraries the CLSID of the object that we want, and COM will find and load the DLL for us.一旦这个标准入口暴露出来了,COM库能够以对象来管理我们的DLL,尽管它实际上还不是COM对象。我们能注册我们的对象—它的CLSID和路径到DLL里—让COM库关注加载DLL。当我们在客户端里使用暗示的DLL链接—链接器添加一些代码来加载指定的DLL,DLL的名称被硬编码写入到客户端的执行文件里。现在,我们会告诉COM库我们需要的对象的CLSID,COM会发现和加载DLL。

The API that COM provides for this purpose is CoGetClassObject: COM为了这个功能提供的函数是CoGetClassObject:

HRESULT CoGetClassObject(
REFCLSID rclsid,                        
DWORD dwClsContext, LPVOID pvReserved
REFIID riid, LPVOID * ppv
);

The first parameter is the CLSID of the object that we want to load. COM looks for it in the registry under HKEY_CLASSES_ROOT\CLSID\{xxx}. 第一个参数是我们想加载的对象的CLSID。COM检查注册表下HKEY_CLASSES_ROOT\CLSID\{xxx}位置来核实有没有。

The second and third parameters give more information about the activation context of our object. For now we will just ask for any server (CLSCTX_SERVER), and pass NULL for lpReserved. 第二个和第三个参数给定我们对象的激活的上下文数据方面的更多信息。现在,我们只是要求为任何服务器模式(CLSCTX_SERVER),对lpReserved设置为空。

The next parameter is the IID of the initial abstract base class pointer that we want to retrieve from the object, and the last parameter is our pointer to a pointer that will receive the pointer to the object.第二个参数是初始的抽象基类指针的IID,这个类指针是我们想从对象里获取到的,最后一个参数是二维指针,会接收对象的指针。

But how does CoGetClassObject find the DLL that implements this CLSID, if the CLSID is just a number (admittedly, a large one) with no encoded information?但是CoGetClassObject怎样查找实现了CLSID的DLL呢,如果CLSID只是一个数字(确实很大)且没有编码信息呢?

1.1.7.3 Standard Object Registration标准的对象注册

All the object-related information that the COM libraries need is concentrated under one entry in the registry: HKEY_CLASSES_ROOT\CLSID. Each object has a key that is named with the string representation of its CLSID. Under this key, the COM libraries look for the information they need in order to create an object. COM库需要的对象相关的信息集中放置在注册表分支HKEY_CLASSES_ROOT\CLSID里。每个对象有一个键,键名是字符串表示了它的CLSID。在这个键下,COM库查找创建对象需要的信息。

For now, we just need one of these entries: InprocServer32. This entry indicates the path to a DLL with the standard entry-point mechanism. Thus, given the CLSID, the COM library can look under HKEY_CLASSES_ROOT\CLSID, find the appropriate key for the object, look for the sub-key InprocServer32, and obtain the name of the DLL.现在来说,我们只需要这些入口中的一个:InprocServer32。这个入口指示了一个指向DLL的路径,这个DLL带有标准的入口机制。这样,给定了CLSID,COM库能够查找HKEY_CLASSES_ROOT\CLSID找到了对象的正确的键,查找到子键InprocServer32,即获取了DLL的名称。

CoGetClassObject then loads the DLL (using LoadLibrary) , obtains the entry point for the DllGetClassObject function (using the Win32 GetProcAddress function), calls DllGetClassObject with the parameters the caller provided, and passes the pointer to the class object back to the client. CoGetClassObject随后加载DLL(使用LoadLibrary),获取DllGetClassObject函数的入口(使用Win32函数GetProcAddress),带参数调用DllGetClassObject,传递指向类对象的指针到客户端。

From there on, everything is just between the object and the client: The pointer returned is an indirect pointer to the vtable of the class factory implemented by the object. [Theoretically the pointer does not even have to be a pointer to a vtable, because it is typed as void. You could simply return a pointer to a standard C++ object (if you don’t need to access functions), any other type of function lookup table, or whatever you wanted. It does not make sense to do so, but I want to make it clear that COM (in the in-process case) does no interpretation whatsoever on this returned pointer.] Any call on this pointer is a normal C++ virtual function call. COM does not interfere in this process; it just provides the service of locating and loading the object. The performance of this sample and the pure vtable-based sample are exactly identical.从哪个观点来看,在对象和客户端之间体现了所有点:返回的指针是一个属于类工厂的非直接的虚函数表指针,这个类工厂是由对象实现的。[理论上讲指针并不需要是指向虚函数表的指针,因为它类型是void。你可以简单地返回一个标准C++对象的指针(如果你不需要访问函数),任何函数查询表的其它类型指针,或你需要的任何类型的指针。这样做确实不符合道理,但是我想让它清晰化,理解到COM(在进程内情况下)对于这个返回指针任何方面是没有解释的]基于这个指针的调用是通常的C++虚函数调用。COM确实没有干涉这个过程;它只是提供了定位和加载服务。这个例子的性能和纯虚函数表例子是相同的。

1.1.7.4 Changes: Step by Step逐步修改

Generate two GUIDs using GUIDGEN.EXE: Choose the third option, "struct . . . GUID", and copy and paste the new GUIDs one by one to interface\bdsrv.h—they are part of the contract between the object and the client (see the Note below). Name them CLSID_DBSAMPLE and IID_IDBSrvFactory, respectively. Leave just the declaration part in "dbsrv.h" ("extern") and put the definition in "object\dbsrvfact.cpp". (Later we will see two other methods for managing GUIDs within source files: one with a macro DECLARE_GUID and the other when we use the Interface Definition Language [IDL] to describe interfaces.) 使用GUIDGEN.EXE来生成2个GUID字符串:选择第三个选项,"struct . . . GUID",且逐次拷贝和粘贴新的GUID到interface\bdsrv.h里—他们是在对象和客户端之间约定的一部分。GUID被命名为CLSID_DBSAMPLE和IID_IDBSrvFactory,。只在”dbsrv.h”里保留声明部分,定义部分放到”object\dbsrvfact.cpp”。(随后我们会看到另外2个在源文件里管理GUID的方法:一个是配合DECLARE_GUID宏工作,另外一个是使用接口定义语言[IDL]来声明接口)。

Note: If you want to be prepared for future needs of GUIDs, and want to have them in consecutive order, take time now to generate some 10 or 20—using the “New GUID” button—and pass them to a separate file somewhere (perhaps a Microsoft Excel spreadsheet?). If you need a lot of GUIDs, you might also want to look at the command-line utility uuidgen.exe, which allows automatic generation of multiple GUIDs (/n parameter). The advantage of having your GUIDs together range from “aesthetic” (all registry keys appear together) to more relevant performance issues (COM needs to do a lookup of GUIDs quite often and works a little more efficiently on clusters of GUIDs than on widely separated GUIDs.注意:如果你想为随后用GUID做准备,且想让它们号连续起来,花些时间来生成10到20个—使用“New GUID”按钮—传送结果到一个独立的文件里(可能是Excel).如果你需要很多GUID,你可能想检查下命令行工具uuidgen.exe,这个工具能够自动生成多个GUID(带/n参数执行)。除了“美学”角度,GUID在一系列范围内连续(所有注册键在一起显示)会在性能上有所提高(COM需要经常对GUID进行查找,成簇安排GUID比散列安排要节省时间)

The following code shows the definition of the GUIDs in object\bdsrvfact.cpp (they look dangerous, but are nothing but really big numbers):下面的代码显示了object\bdsrvfact.cpp文件里的GUID定义(他们看起来危险,不过只是大数字而已)

// {30DF3430-0266-11cf-BAA6-00AA003E0EED}                       
static const GUID CLSID_DBSAMPLE =                       
{ 0x30df3430, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } 
};                       
// {30DF3431-0266-11cf-BAA6-00AA003E0EED}                       
static const GUID IID_IDBSrvFactory =                       
{ 0x30df3431, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } 
};
  • Remove the declaration of DllGetClassFactoryObject in dbsrv.h (already declared in OLE2.H), and change the implementation in dbsrvfact.cpp to the following:删除drsrv.h里的DllGetClassFactoryObject声明(在OLE2.H里有声明),且修改drsrvfact.cpp里的实现代码:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppObject)

Validate the two parameters as CLSID_DBSAMPLE and IID_IDBSrvFactory, respectively. If one of them does not have the expected value, return CLASS_E_CLASSNOTAVAILABLE or E_INVALIDARG, as follows:分别审核2个参数看是不是CLSID_DBSAMPLE和IID_IDBSrvFactory。如果有一个不是期待的值,分别返回CLASS_E_CLASSNOTAVAILABLE或E_INVALIDARG。

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppObject) {
if (rclsid!=CLSID_DBSAMPLE) { 
// Is this the number of our class?
return CLASS_E_CLASSNOTAVAILABLE;
}
if (riid!=IID_IDBSrvFactory) { 
// Is this the number of our interface?                       
return E_INVALIDARG;                       
}                       
*ppObject=(IDBSrvFactory*) new CDBSrvFactory;                       
return NO_ERROR;                       
}

Include ole2.h as follows: In stdafx.h append "#include <ole2.h>". Add "#define _AFX_NO_BSTR_SUPPORT" before the other includes, because the MFC header files define some symbols differently than OLE2.H does. (Be careful not to include afxole.h, because doing so provides an ASCII/Unicode mapping layer, which can get into your way when using some COM interfaces or APIs. This mapping layer was removed in MFC 4.0.)用如下方法包含ole2.h:在stdafx.h里添加"#include <ole2.h>"。在其它包含语句前面加上"#define _AFX_NO_BSTR_SUPPORT",因为MFC头文件定义的一些标志和OLE2.H里定义的不一样。(要注意不要调用afxole.h,因为包含后会有一些ASCII/Unicode转换函数,这会妨碍你使用COM接口或API。这些转换函数在MFC4.0里删除了。)

Create a module definition file, DB.DEF, and export DllGetClassObject. (You cannot use _declspec(dllexport) because the function is already declared in ole2.h with different modifiers.)创建一个模块定义文件,DB.DEF,且导出DllGetClassObject。(你不能使用_declspec(dllexport)因为这个函数已经在ole2.h里以不同的修饰进行了声明)。

For the client, in client\dbdoc.cpp, call COM instead of loading our DLL directly: Call CoGetClassObject instead of DllGetClassObject. Validate the result using the FAILED-macro provided by OLE2.H:对于客户端来说,在client\dbdoc.cpp里,调用COM接口来替换直接加载DLL:调用CoGetClassObject来替换DllGetClassObject。用FAILED宏来检查结果,这个宏是OLE2.H里提供的:

HRESULT hRes;
hRes=CoGetClassObject(CLSID_DBSAMPLE, CLSCTX_SERVER, NULL,
IID_IDBSrvFactory, (void**) &pDBFactory);                       
if (FAILED(hRes)) 
{CString csError;
csError.Format(_T("Error %d creating DB Object!"), hRes);
AfxMessageBox(csError);
return FALSE;                       
}

Call CoInitialize(NULL) in CDBApp::InitInstance to initialize the COM libraries; and call CoUninitialize() in CDBApp::ExitInstance.在CDBApp::InitInstance里调用CoInitialize(NULL)来初始化COM库;在CDBApp::ExitInstance里调用CoUninitialize()来释放。

Add the definitions of CLSID_DBSAMPLE and IID_IDBSrvFactory to the beginning of dbdoc.cpp. Instead of hard-coding the name of the DLL, we now hard-code the CLSID of the object that we want to use!在dbdoc.cpp里开始位置添加CLSID_DBSAMPLE和IID_IDBSrvFactory的定义。代替DLL的名称的强制指定,我们现在强制指定了COM对象的CLSID。

Add ole32.lib to the project and remove db.lib from the project (in Project/Setting/Linker).添加ole32.lib到工程里且删除db.lib。

Change stdafx.h to include both ole2.h and #define _AFX_NO_BSTR_SUPPORT.修改stdafx.h来包含ole2.h和#define _AFX_NO_BSTR_SUPPORT。

Register the DLL in register HKEY_CLASSES_ROOT\CLSID\{<<your clsid>>}\InprocServer32={your path}db.dll. (If your DLL is in your system path or in the same directory as the client, you do not need to specify the complete path to the DLL.). All GUIDs in the registry appear in ASCII form within brackets. You can get your GUID's string representation from the definition provided by GUIDGEN (should be in your header file). 注册DLL到键HKEY_CLASSES_ROOT\CLSID\{<<your clsid>>}\InprocServer32={your path}db.dll。(如果你的DLL在系统目录里或和客户端同一个目录,你不需要指明DLL的路径。)所有在注册表里显示的GUID是以带括号的ASCII形式。你能从GUIDGEN提供的定义来获取你的GUID的字符串(字符串应该旋转到你的头文件里)。

a. Run Regedt32.exe and open HKEY_CLASSES_ROOT\CLSID.运行regedit.exe且打开HKEY_CLASSES_ROOT\CLSID.

b. Add a Key with the name of your CLSID:添加你的CLSID名称对应的键。

{30DF3430-0266-11cf-BAA6-00AA003E0EED}

c. To this new Key add an unnamed Value, with Data Type = REG_SZ and String = DB Sample Object. This string is not used by COM but can help you to find your CLSID later.在这个键下添加一个未命名值 ,数据类型为REG_SZ且字符串内容为DB例子对象。这个字符串不会使用,但是会帮助你查找到你的CLSID。

d. Add a Key named InprocServer32.添加一个命名为InprocServer32的键。

e. Add an unnamed Value, with Data Type = REG_SZ and String = <path>\db.dll, replacing <path> with the actual path of your DLL. Optionally you can just register db.dll and add its path to the system or user path.添加一个未命名值,数据类型为REG_SZ,字符串内容为<path>\db.dll,<path>用你DLL所在的实际目录代替。可替换的方案是,只命名为db.dll,把路径添加到系统或用户目录下。

Compile the object and the client. (This time the order does not matter because we no longer link to the DLL.) If you receive errors when creating the object, you can look up the error codes (hRes) in WINERROR.H. The most probable failure is an incorrect registration of the DLL or a non-exported entry point for DllGetClassObject. (Of course, this has never happened to me!)编译对象和客户端。(这时候先编译哪个已经不重要了,因为不需要链接DLL了。)如果你在创建对象时遇到错误,你能检查错误编码(hRes),在WINERROR.H里检查对比。最有可能的错误是DLL的非正确注册或一个为DllGetClassObject服务的未导出的入口点(当然,对我来说,这永远不会发生)

You can still mix and match Unicode/ASCII and Debug/Release clients and servers.对于客户端和服务端的Unicode/ASCC和Debug/Release,你依然能够混用或配对用。

1.1.7.5 不能创建对象80040154

在注册表里检查手工配置项:

DB

把ThreadingModel加上

再加上

DB Sample Object

DB Sample Object还是不行。

否定了注册表配置上的问题。

用下面例子里的代码生成的DLL,再用注册软件regsvr32进行注册,执行本节客户端80040154不再出现,但是出现:

DB error

用下个例子对应的客户端就正常了。

现在再看注册表:

regsvr32

REG_RZ

从上面总结2点:

1、注册表里在HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\下也要注册,不过这个键是个连接,不是实际的项,ROOT下有,这里就会有。。

2、DLL里不但只有一个函数了,还有多个了。

现在继续做实验,把DLL里的函数,只修改为一个,看情况怎样:

下个例子里的客户端正常,本节例子客户端提示80070057表示类型不一致。

再把本节例子DLL做为COM组件来试验,结果:

提示有非法操作

把InprocServer32下的ThreadingModel加上试验下不行,有非法操作,在ROOT键下加入DB Sample Server的键也是不行。除非把全部相关键全删除,再重新来才没有错误。虽然没错误,但是还是会报80040154错误。经过观察发现是注册表里的CLSID有冲突,有2个一样的,这样导致了非法操作。还有就是把下节例子里的DLL服务器注册上后,也不会有非法操作,只是会报80070057错误,这个错误是接口的ID对应不上时会报的。

再用本节的注册表配置加下节的例子代码试下:

提示80040154

再用regsvr32试下,没有问题。但是注册表里好象有2条。发现CLSID下的项名里一个带{},一个不带{},不带{}肯定是有问题的。我在开始做时,忽视了{},这是个错误。通过第一个图和最后一个图里就能找到差异了。

注意加了{}后,使用本节例子,还是有非法操作。但是至少肯定80040154是因为少带了{}造成的。

现用下节例子的DLL和注册工具软件注册下,然后恢复为本节的DLL。还是有非法操作,这说明注册表里的内容是不影响非法操作的事的。

修改代码,添加提示看看:

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppObject) {
       ::MessageBox(NULL,_T("start"),_T("start"),MB_OK);
       if (rclsid!=CLSID_DBSAMPLE) {
              return CLASS_E_CLASSNOTAVAILABLE;
              }
       if (riid!=IID_IDBSrvFactory) {
              ::MessageBox(NULL,_T("222"),_T("222"),MB_OK);
              return E_INVALIDARG;
              }
       *ppObject=(IDBSrvFactory*) new CDBSrvFactory;
       return NO_ERROR;

}

发现start能够正常执行出来。

把注册表里的DLL指向修改为工程调试目录下的文件,即InprocServer32下默认数据为:

H:\专题讲座\COM\例子\msdn c++ into com sample\DBAlmostCOM\Object\WinDebUn\DB.dll

把DLL工程设置为Debug模式,执行文件指定为客户端的调试文件,加断点调试。发现:

db.dll_debug

发现是delete this的问题。

这个delete this是上节例子里都有的,怎么会有错误呢?

从压缩包里再解压出来,还是不行。

把断点设置到delete this上,发现调用的不是客户端的

pDBFactory->Release(); // do not need the factory anymore

代码行。

而是
       hRes=CoGetClassObject(CLSID_DBSAMPLE, CLSCTX_SERVER, NULL, IID_IDBSrvFactory, (void**) &pDBFactory);

因为是演示思想的例子,再加上下节例子没有非法操作,所以不再详细跟踪问题了。
友情链接
版权所有 Copyright(c)2004-2021 锐英源软件
公司注册号:410105000449586 豫ICP备08007559号 最佳分辨率 1024*768
地址:郑州市金水区郑州大学北校区院(文化路97号院)内