VC自制COM组件

Com组件这东西实在是麻烦,以前也一直没有碰过。

过程是这样子的,一天在百度知道上看到有人求助一段C++代码转成VB。我又不熟悉VB,于是就顺手给封装成DLL给了他。

然后提问者回复说,能不能给搞成能用CreateObject方式创建对象的那种方式来调用。这一看,应该要com吧?但是我不会写com。想着稍微掌握一点以后可能也能用到,就去找教学啥的稍微尝试一下。

主要参考的文章是 http://icodeguru.com/VC%26MFC/InsideAtl/ch02c.htmhttp://www.codeguru.com/cpp/com-tech/activex/tutorials/article.php/c5567/Step-by-Step-COM-Tutorial.htm 这两篇。一开始是看后者,后面也看了前者,结合起来,大概理清一点思路了。

因为COM组件是面向对象的,所以要制作COM组件前,先确定要有什么类。具体百度知道上那个问题在这里 http://zhidao.baidu.com/question/551068312.html 。根据教学,写了一个idl文件,定义如下内容

import "unknwn.idl";

[
uuid(EB35191C-DCC7-49DA-BC10-A1E0E9CEECA8),
object,
helpstring("Get Hash code for 156542994")
]
interface IGetHash : IUnknown
{
HRESULT GetHash([in]BSTR szStr, [out,retval]LONG* pOut);
}

[
uuid(AB18183F-A79B-49E7-92E8-514A056B2FCB),
helpstring("Library for GetHash, by 156542994")
]
library MyGetHashLib
{
importlib("stdole32.tlb");
interface IGetHash;
}

里面的uuid是用软件生成的。写了这么一个gethash.idl以后,用midl工具处理一下,生成gethash_i.c, gethash.h, gethash.tlb等文件。

然后是类的编写,从IGetHash派生出CGetHash类,写上GetHash的实现,以及从IUnknown里面来的AddRef的实现、Release的实现、QueryInterface的实现。这些函数都是stdcall方式调用的。AddRef和Release的实现没什么好说,就是智能指针而已,在Release到0的时候要释放,delete this ←这样。

至于QueryInterface,它的意思是当前对象能不能转换成另一个接口的指针。因为我这个CGetHash的父类有IUnknown和IGetHash,所以当参数中指定的那个iid为IID_IUnknown和IID_IGetHash的时候,调用AddRef并给输出参数赋值,同时返回S_OK;否则就输出参数置0。

这样这个类就写出来了,不过还没办法用。COM组件用了工厂模式,要为这个类写一个工厂才行。于是从IClassFactory派生出一个CClassFactory,实现IUnknown的AddRef、Release、QueryInterface和实现IClassFactory的CreateInstance即可。其中CreateInstance的第一个参数pOuter,这个到底是COM的什么特性我还不知道,也没有用到,总之按照教学说的如果这个不是0就返回CLASS_E_NOAGGREGATION好了。然后CreateInstance里面的实现要偷懒一点,就直接创建出CGetHash对象,然后用它的QueryInterface把iid和pOut送进去,之后Release。因为如果参数符合,那么在QueryInterface里面会增加引用计数,Release的时候也就不会被删掉。这样CreateInstance也搞定了。LockServer是啥我不知道,看msdn上介绍似乎是什么lock了以后能更快还是啥的……总之直接返回S_OK。有说法是Lock的时候调用AddRef然后Unlock的时候调用Release就行。不过不太清楚……刚学

dll形式的COM的最重要接口DllGetClassObject,这个接口是用来获取工厂的。三个参数:clsid、iid和pOut。其中clsid代表类的id,就是刚刚那个CGetHash。也就是说不光是IGetHash要有id,这个CGetHash尽管不对外暴露但是也还是要有id。于是用id生成器给它生成一个id。到时候这个函数被回调的时候,比如VB里面用了CreateObject来创建它,clsid会送进来CLSID_CGetHash,iid会送进来IID_IClassFactory,判断好这个情况,其他时候丢个CLASS_E_CLASSNOTAVAILABLE就行。

至于那些Register和Unregister要写不写都行,写的话就你自己维护,不写的话就用户手工去改注册表或者什么的都可以,总之在HKEY_CLASS_ROOT里面添加一个代表你类的名字的键值比如MyGetHashLib.GetHash,在下面建立一个CLSID把你类(CGetHash)的id写进去,然后在HKEY_CLASS_ROOT的CLSID下面建一个你类的id为名的子键,下面创建一个ProgID一个InprocServer32,前者一样填MyGetHashLib.GetHash,后者填你dll的完整路径,大功告成

在VB里面添加引用之后就可以这么调用:

Dim a As IGetHash
Set a = CreateObject("MyGetHashLib.GetHash")
MsgBox Hex(a.GetHash("cc"))

比用Declare那个看起来是不是高端洋气了?w

把tlb文件内嵌进去的话,根据 http://support.microsoft.com/kb/122285/en-us 这篇文章的说法,资源id设1,类型typelib即可。不过这样就有一个疑问,因为xp style的时候也要求资源id设1……如果想同时都要的话不知道应该如何处理呢。

下载代码: gethashcom

之后我想了想,既然它都是通过QueryInterface来的,那么我只要在它Query的时候返回正确的接口就成了,现在既然这个类这么简单,那我把工厂和产品合起来也未尝不可。

于是出来了下面这个把IClassFactory和IGetHash合起来的版本

下载代码: gethashcom2

画了个示意图,希望能好懂一些。以后自己要是忘了也可以参考

DllCanUnloadNow最好也实现一下(本例中没有写这个……),目测判断这个dll创建出来的对象是不是全部已经全部没有引用了,如果是,那么就返回S_OK;不是,就返回S_FALSE

发表评论