一丶我们要理解COM是什么(为什么理解)
现在很多人会用com(也就是ALT)但是不知道原理,如果改一点东西,那么整体的框架重来,因为你不懂改哪里,如果懂了,那么遇到问题,那么就会知道我要怎么做,是什么问题了
二丶什么是COM
COM是微软公司为了计算机工业的软件生产更加符合人类的行为方式开发的一种新的软件开发技术。在COM构架下,人们可以开发出各种各样的功能专一的组件,然后将它们按照需要组合起来,构成复杂的应用系统。由此带来的好处是多方面的:可以将系统中的组件用新的替换掉,以便随时进行系统的升级和定制;可以在多个应用系统中重复利用同一个组件;可以方便的将应用系统扩展到网络环境下;COM与语言,平台无关的特性使所有的程序员均可充分发挥自己的才智与专长编写组件模块;等等。 COM是开发软件组件的一种方法。组件实际上是一些小的二进制可执行程序,它们可以给应用程序,操作系统以及其他组件提供服务。开发自定义的COM组件就如同开发动态的,面向对象的API。多个COM对象可以连接起来形成应用程序或组件系统。并且组件可以在运行时刻,在不被重新链接或编译应用程序的情况下被卸下或替换掉。Microsoft的许多技术,如ActiveX, DirectX以及OLE等都是基于COM而建立起来的。并且Microsoft的开发人员也大量使用COM组件来定制他们的应用程序及操作系统。
好,这是百度的答案,很多人看了懵逼,简单来说
总结:
1.COM是一种框架,我们可以利用这个框架,实现跨平台开发,比如你开发了一个COM,那么别的程序一样使用
2.COM其实是二进制下的可执行的程序,可以给其他的程序使用
实现简单的COM从接口设计模式开始
什么是接口模式
1.接口模式就是我们不知道,但是当用户用的时候,才知道是什么类型,所以可以是已知的,规范一下接口即可.
简单来说:
接口模式就是类似于U盘 插入到电脑上,中间的USB的那个接口,只要是支持这个接口的,都可以插入到电脑上
比如硬盘等等.
2.插件模式: 插件模式是未知的,比如用户怎么写你都是不知道的,所以定义好规范,让用户一一的实现你的插件的接口即可.
简单来说:
简单来说就是为你的程序提供的扩展,如果用户实现了你自定义的接口,那么你的应用程序就可以支持这个功能了.所以插件和接口不要搞混
实现简单的COM以及思路
1.按照上面所说的,我们要实现COM那么就要有一个接口,这里我用C++来写了,
2.在写的过程中,我会依次的把为什么这样写,不能怎么写都会说清楚,最后开发一个跨语言使用的ATL(也就是COM)组件
3.下面的内容可能有点多,最后我会写总结,可以看下.虽然函数不多,就一个类,但是从底层讲起,为什么这么做所以比较多.
1.定义接口类
class IUnKnow
{
public:
virtual HRESULT QueryInterFace(const GUID& riid,void **ppObject) = 0;
virtual ULONG AddRef() = 0;
virtual ULONG Release() = 0;
}
首先将第一个接口中的函数
QueryInterFace,这个函数是查找我们的接口,根据查找的接口通过第二个OUT参数接受查询接口的实现类的对象
什么意思?(GUID下面讲解)
其实就是我定义了一个新的接口类,继承了IUnKnow,这个新的接口类中有自己新添加的功能,而有一个类是实现了这个接口类,通过这个函数,可以找到实现类的对象,进而可以调用里面的方法(下面讲解)
为什么要这样写返回值,以及参数要这样写:
想一下,如果我们返回值是void *的话是不是不需要第二个参数了,是不需要第二个参数了,但你保证所有的语言
都会这样返回吗,显然是不会的,所以要统一接口,统一返回值(HRESULT)参数由第二个传出
AddRef() 引用计数 这个必须加,因为你想,如果我们每次查询是否存在就new一个对象,那样是不是太浪费了
所以搞个引用计数
Release();大家可能不同了,为什么释放资源要单独写一个Release()释放,这里请看下面讲解
接口的设计原则
1.接口一旦是定义好的,你的函数的顺序不能改变 为什么?
因为接口的设计都是用的指针,都是虚表去查(虚表是什么,可以补一下C++的基础,简单来说就是通过虚函数来调用的)如果一单你的接口的顺序改变了,那么对应的虚表就会改变,
举个例子:
比如你的插件(也就是咱们现在写的这个)有一个功能是Add(int n1,int n2,long *Result) (两个数相加)
你编译好了你的插件了,我的Client程序就可以使用了,使用的时候正常调用Add,返回的结果也就正确了,
如果有一天你有一个减法,正好放在的Add的前边,那么你的Client就会调用减法了,因为以前的那个位置是输入Add的这样就会出错了.
2.参数不能改变
参数也是不能改变的,接口一点定义了,就不要动了.
3.兼容性
什么是兼容性
比如你的Client是老版本,用你编写的新插件,你的Add函数没有变,还是会依次调用你的函数,不影响使用.
但是你要反过来想,当你的Client是新版本的时候,调用你的旧的插件怎么办,比如你的旧的插件没有Sub函数
而你调用了是不是就出错了.
所以为了保证兼容,我们会新定义一个接口类,让以前的实现类继承新的接口类,而新的接口类继承以前的接口类
伪代码:
新版本的插件要这样写
class InterFaceMathOld : IUnKnow
{
virtual Add(...) = ;
}
class InterFaceMathNew : pulic InterFaceOld
{
virtual Sub(....) = ;
}
class CMath : public interfaceMathNew
{
Query....(){}
AddRef(){}
Release(){}
Add(...){}
Sub(...){}
}
老版本的插件是这样写
class InterFaceMathOld : IUnKnow
{
virtual Add(...) = ;
} class CMath : public InterFaceOld
{
Query....(){}
AddRef(){}
Release(){}
Add(...){}
}
这些代码我都会写好,发到云盘中下载研究,如果连接过时了,请评论告知,或者QQ2510908331 这地方一定要细看,
我会一步步的吧Com从基本到高级的源码写出来,这样能熟悉一下COM的框架
接口设计的细节问题 (解决为什么要用Release)
1名称粉碎
.我们想一下,C语言的函数,看一下二进制或者DLL,是不是都是原本什么样子就是什么样子
C++的函数有一个语法支持重载了,内部怎么支持的重载,这就是一个问题
C++为了支持重载,会对你定义的函数做一个名称粉碎,也就是加了一些额外的符号,比如我们调试中都会遇到一种错误叫做,找不到外部符号 例如test@aHINTAdd....当然我写的不准确,下面有一个帖子专门介绍,可以看看
http://www.cnblogs.com/zhugehq/p/5959360.html
不同的编译器它的名称粉碎是不一样的,这个是没有标准的,你说有重载,这个是标准可以,但是怎么实现的,没有标准
所以我们为什么要定义为纯虚函数,这样你要调用函数就直接通过虚表,去查找了,而不是找你的实现了,所以我们的接口没有重载,不能写任何重载的函数
从逆向角度说一下,为什么不支持重载,因为VC++6.0编译器,在建立虚表的时候,会根据函数的类型排序,上面的接口原则说了,不能影响顺序问题,所以不能写
2.多重继承问题
a.多重继承会影响虚表的,一旦影响了虚表,就改变的接口的不变原则,(虚继承也是一样).
b.不能有虚析构,如果你调用虚析构,VC++6.0会传入一个1,或者一个0(后台传入的,逆向角度)这个根据这个状态值去释放内存.
而GCC是不一样的,他可以用数组的方式,反正都可以实现,那么这样也违反了接口的设计原则了.
总结:
1.不能用多重继承(父类没有虚函数可以多重继承,不影响子类的虚表即可)
2.不能用虚析构,如果释放内存,则用纯虚方法的Release()来释放内存
3.不能有重载,重载会影响虚表
GUID简介
GUID是一种数据结构,在Windwos系统中可以通过guidgen命令来打开GUID,也可以通过API coCreateguid(查一下MSDN)我都是用windows自带的
作用: 我们第一个查找接口的函数中定义了Guid,这是为了我们查询接口准备的,每一个接口都会有一个GUID,guid是保证不重复的.
总结:
说了怎么多,我们发现其实定义一个COM的接口很容易,就是2个接口,(IUnknow(*的接口类),Ixxx(你自己的接口类))和一个实现的接口类,我们就要说怎么多细节.当然这也是为了我们更加的理解COM的设计.
如有问题QQ:2510908331 论坛:www.w1x8.com 如果说的有错误,请指出,如果你有更好的建议,请评论说明,因为COM组件很老的,很多人会用,但是不懂,因为我们是逆向工程,所以需要熟悉COM框架,为了谁让更多人熟悉COM框架,请欢迎指出
框架代码连接: 链接:http://pan.baidu.com/s/1dFkvwJn 密码:myie
为了让大家学习方便,所以没有写引用计数和释放内存,所以框架代码释放内存可能会出错,请先掌握这个第一个版本的框架代码,后续会慢慢优化,基于这个框架代码.配合博客文章,能更深的理解COM框架