http://www.codeproject.com/Articles/37044/Writing-a-BHO-in-Plain-C

介绍

Browser Helper Objects (也被称为 BHOs) 是com组件,扮演着ie插件的角色。BHOs可以在某种程度上定制IE,如:用户交互的修改,网页过滤的及下载管理。

这篇文章,我们将学习如何编写并安装一个简单的BHO,用原始C++而不是其他的框架,如ATL或MFC。

 

背景

COM-(组件对象模型)是一个语言中立的技术,广泛的应用于windows,更利于软件模块化和软件重用。

大部分COM的代码用用ATL或MFC框架来完成相应的工作(本文介绍原始C++编写)。然而,学习创建COM对象ATL和MFC常成为另外一个障碍,尤其对于像BHOs这样简单的COM来说。

这篇文章将介绍编写你自己的BHO需要知道的关于COM和BHOs相关知识。只用原始C++和windows API 来完成BHOs的编写,不需要复杂的框架如ATL或MFC。

 

开始

理解这篇文章的最佳途径是下载并阅读上面链接提供的源代码。源码已经充分注释且易于理解。

 

理解COM代码

COM术语

首先,让我们了解COM技术的梗概:

接口--一系列对其他对象可见的方法。它等同于C++纯虚类里的共有函数。

组件对象类--继承于一个或更多的接口,并重写所有的方法。它等同于一个从一个或多个纯虚类继承的C++实现类。

对象--一个组件对象类的实例。

GUID--全局唯一ID--GUID是一个128位的唯一数字。它可以通过guidgen.exe工具生成。

IID--接口ID--标识一个接口的GUID。

CLSID--类ID--一个标识COM组件的GUID 。你可以找到关于BHO‘s类ID那个例子,在common.h文件中叫做CLSID_IEPlugin_Str.每一个COM组件有一个不同的标识,如果你打算在例子的基础上重写你自己的BHO,你应该生成一个新的ID。

 

IUnknown 接口

为了能创建组件对象,我们必须编写用来实现接口的组件对象类。所有COM对象必须实现一个叫做IUnknown接口。这个接口有三个最基本的函数,这个函数允许其他对象管理组建实现类和用来访问其他接口的对象。这三个函数分别为QueryInterface, AddRef, 和Release. 既然所有可变的组建实现类我们都要从IUnknown继承实现。也就是说要创建这个组建实现类CUnkinown并且使所有我们其他的具体实现类继承它,这样我们就不需要为每个具体实现类单独实现IUnknown接口了。

COM DLL 导出

每个COM DLL 导出四个函数被COM系统用来从DLL创建并管理COM对象还有安装和卸载DLL。这些函数是:

 

  • DllGetClassObject
  • DllCanUnloadNow
  • DllRegisterServer
  • DllUnregisterServer

备注:你可以找到这些函数在main.cpp文件中,同时它们也在dll.def文件中声明。

 

我们的DLL必须有一个IClassFactory 接口的具体实现类。我们把这个具体实现类称作CClassFactoryDllGetClassObject 函数创建CClassFactory对象并且返回指向它们的接口指针。IClassFactory接口更多的细节将被简单的介绍。

 

DllCanUnloadNow函数被COM调用用来决定我们的DLL可以被从一个进程中卸载。我们所要做的就是检测我们的DLL当前是否在管理其他对象。如果不是返回S_OK,否则返回S_FALE。我可以在我们的具体实现类的构造函数中自增DLL的全局引用计数器(DLLRefCount)在析构函数中自减全局引用计数器(DLLRefCount)。如果引用计数器为不为0,就意味着我们的具体实现类的实例仍然存在,此刻不能卸载dll。

 

DllRegisterServer 被一个程序调用,程序需要dll自我安装。我们必须在系统里注册我们自己的COM组件同时也作为BHO。我们通过创建一下的注册入口做到这些:

HKEY_CLASSES_ROOT\CLSID\<CLSID_IEPlugin_Str> —这个建的默认值应该被设成COM组件的可读描述。本例中,它是"CodeProject Example BHO"。

HKEY_CLASSES_ROOT\CLSID\<CLSID_IEPlugin_Str>\InProcServer32 —这个键的存在标识这这个COM组件可以被作为DLL加载到需要使用它的进程里。我们需要为这个键设两个值,如下:

(default)  REG_SZ or REG_EXPAND_SZ 值指向包含COM组件的DLL路径。

ThreadingModel —这指定COM组件的线程模型。它是一个更超前的概念,我们不需要担心它。我们仅需要把它设为"Apartment"。

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ Explorer\Browser Helper Objects\<CLSID_IEPlugin_Str> —这个键的存在以BHO的形式注册我们的COM组件。我们在名为NoExplorer键下创建一个值,把键设为REG_DWORD类型用1填充。通常BHOs也被通过explorer.exe加载,而这个值阻止被explorer.exe进行不必要的加载。

DllUnregisterServer 被调用来做和DllRegisterServer完全相反的功能--反注册我们的COM组件同时以BHO的形式删除它。为了做到这些,我们只需要删除我们在DllRegisterServer创建的注册表键值。

 

  

IClassFactory接口

 

COM用由我们的DLL里的DllGetClassObject 函数创建的IClassFactory对象来获取由DLL支持的接口实现的实例。IClassFactory接口定义了CreateInstance和LockServer的方法。我们调用我们继承自IClassFactory 接口的具体实现类CClassFactory

 

备注:你可以在ClassFactory.h中找到的CClassFactory的定义 ,在ClassFactory.cpp中找到实现。

 

 

CreateInstance 确如它所言--用我们支持给定接口ID的DLL创建一个具体实现类的实例。因为我们是BHO,我们只需要创建一个具体实现类的实例,这个实例支持IObjectWithSite 接口。

 

LockServer 被调用来锁或者解锁内存里的DLL。根据它是否将要解锁,我们LockServer 的实现只是简单的DLL全局引用计数DllRefCount变量的自增或自减。

 

理解BHO代码

IE如何加载BHO

当BHO将被IE加载的时候,它调用一个叫做CoCreateInstance的COM函数,传给它我们BHO的类ID和叫做IObjectWithSite的接口ID。IObjectWithSite在下文将被更多的介绍。COM相应地把我们的dll加载到IE进程里通过查找注册表里的类ID,然后调用我们暴露的接口DllGetClassObject函数去获得我们的CClassFactory 具体实现类实例。一旦COM有一个指向我们CClassFactory 对象的指针,COM调用它的CreateInstance方法,传给它ie提供的接口id。我们的CreateInstance实现创建一个IObjectWithSite实现的实例,这个实例叫做CObjectWithSite,并且通过它获得了所请求的接口id的接口指针,然后把接口指针返回给COM,COM又把接口指针传给IE。IE然后用IObjectWithSite接口指针和我们的BHO交互。

 

 

IObjectWithSite接口

BHOs被要求实现IObjectWithSite接口,是IE用来和BHO交互的。这个接口有两个方法 SetSite 和 GetSite.我们DLL里CObjectWithSite具体实现类实现了IObjectWithSite接口。

备注:你可以在ObjectWithSite.h文件里查找CObjectWithSite的声明。在ObjectWithSite.cpp中查找实现。

SetSite 被IE调用给我们一个网站对象的指针。网站对象就是一个COM对象,这个对象由IE创建并且可以被我们的BHO用来和IE通信。我们的SetSite的实现从网站对象那里获得一个指向IConnectionPointContainer接口的接口指针。我们然后用在IConnectionPointContainer接口里的一个方法FindConnectionPoint获取一个IConnectionPoint接口指针,这个指针指向一个支持DWebBrowserEvents2的Dispatch接口的对象。dispatch接口是一种特定的接口,这种接口派生自IDispatch并且通过它的Invoke方法接受事件通知。我们DWebBrowserEvents2的实现叫做CEventSink,下一节有更多的细节介绍。我们用IConnectionPoint接口的Advise方法告诉IE传事件到我们的CEventSink对象。

我们SetSite的实现也从网站对象那里获得了指向IWebBrowser2接口的接口指针。那个IWebBrowser2接口被IE的网站对象实现并且有让我们和IE交互的方法。

备注:因为我们这个BHO的例子只是从IE接收事件通知,不是真正的控制IE,所以我们不需要用IWebBrowser2里的任何方法。然而,我引进了获取IWebBrowser2接口的代码,这样你就可以在你需要的时候在你自己的BHO中用它。你可以在这里找到IWebBrowser2的相关文档。

GetSite被IE用来知道我们已经设置为网站对象的那个对象。IE传给我们一个接口ID给一个接口,这个接口需要我们当前设置的网站对象,我们只是简单的从我们的网站对象那里获得那个接口并把它返回这IE。

 

CEventSink Coclass

CEventSink具体实现类是我们DWebBrowserEvents2的IDispatch接口的实现。DWebBrowserEvents2派生自IDispatch但没有实现它自己的任何方法。相反,DWebBrowserEvents2存在只是为了它的DIID (dispatch IID)能够存在。这个DIID可以 识别DWebBrowserEvents2具体实现类接收自它自己的IDispatch的Invoke的事件。

备注:你可以在CEventSink.h中找到CEventSink的定义,在CEventSink.cpp中找到实现。

当IE需要通知我们一个事件,它会调用CEventSink的Invoke方法,用dispIdMember参数传事件ID并用pDispParams 参数传递其它事件信息。IDispatch除Invoke之外还有其他三个方法(GetTypeInfoCount, GetTypeInfo, 和GetIDsOfNames)。但我们不需要为它们实现任何功能,因为我们只是接收事件。

另一个你可能意识到关于CEventSink的区别可能是:我们别的具体实现类都是派生自CUnknown。这是因为我们只需要一个CEventSink的DLL型全局静态实例EventSink。因为我们不需要实现任何引用计数功能,因此我们不需要CUnknown函数的引用计数和内存管理的功能。

 Handling Events

 IE调用CEventSink::Invoke 通知我们事件。dispIdMember参数包含一个将要被触发的事件的ID,pDispParams->rgvarg[]数组包含VARIANTs类型的事件自身的参数。你可以在DWebBrowserEvents2文档里找到一个事件携带的任何参数,这个文档可以在这里被找到。参数被已列在事件文档里的相对顺序传到pDispParams->rgvarg[]数组。为了把这些参数从一个VARIANT转换为一个更为有用的类型,我们首先为每个将要使用的参数声明一种VARIANT类型并用VariantInit API 函数初始化它们。.然后,我们可以用VariantChangeType API 函数转换pDispParams->rgvarg[]数组里的VARIANT为一种更有用的VARIANT类型。一旦我们为所有我们需要的参数完成了这些转换,我们就能传参数值到我们自己的Event_*方法里,通过用我们的转换的VARIANT变量的数字。在事件处理方法返回是,我们通过对每个调用 VariantClear 方法释放我们使用的VARIANT的所有资源。如何做到这些的具体实例将在下面给出。

 

BeforeNavigate2 事件

我们的BHO例子只是处理一个事件,BeforeNavigate2 事件。BeforeNavigate2 文档可以在这里找到。这个事件记载IE导航到一个新的地址前被触发。

看下文档,我们发现BeforeNavigate2 给我们几个参数。我们将不关心pDisp参数,这个参数只是一个指向网站对象的IDispatch接口指针。

事件参数被作为变量装在pDispParams->rgvarg[]中。在我们可以用这些参数之前,尽管我们需要把那些变量转换成更容易使用的变量类型。我们首先需要用VariantInit初始化每个变量,当我们完成时我们通过VariantClear函数释放所有使用的资源。

 

1
2
3
4
5
6
7
8
VARIANT v[5];
// Used to hold converted event parameters before
// passing them onto the event handling method
 
...
for(n=0;n<5;n++) VariantInit(&v[n]); // initialize the variant array
... // use the variant array here
for(n=0;n<5;n++) VariantClear(&v[n]); // free the variant array

 那个url参数包含将要被浏览的URL。他是第五个参数(从0计数),因此我们用访问pDispParams->rgvarg[5]它。我们把这个变量转换成VT_BSTR类型,因为它可能不是那个格式,然后存放转换后的变量如v[0].我们通过v[0].bstrVal VT_BSTR的方式可以访问URL。BSTR字符串在COM里传递字符串数据很常用。它是由标识字符串长度的四个字节的前缀和紧接其后的双字节以0结尾的Unicode字符串类型的字符串数据组成。BSTR变量总是指向字符串数据,不指向它之前的4-字节的前缀。很方便我们用C-风格的字符串使用它。一个双字节的字符串类型LPOLESTR在windows API的头文件里有声明,忽略程序