都内で働くSEの技術的なひとりごと

都内でサラリーマンやってます。マイクロソフト系(たまに、OSS系などマイクロソフト以外の技術も...)の技術的なことについて書いています。日々の仕事の中で、気になったことを技術要素関係なく気まぐれに選んでいるので記事内容は開発言語、インフラ等ばらばらです。なお、当ブログで発信、発言は私個人のものであり、所属する組織、企業、団体等とは何のかかわりもございません。ブログの内容もきちんと検証して使用してください。よろしくお願いします♪

『 Google Chrome Frame をチェックしていたら、BHO ( Browser Helper Object ) が気になってきた 』 ので、C++、ATL を使って実装してみた

 以前投稿した BHO に関する記事 『Google Chrome Frame をチェックしていたら、BHO ( Browser Helper Object ) が気になってきた』は、ほとんど Surface に関する記事になってしまったので、今回は寄り道せず、まじめに C++ で BHO ( Browser Helper Object ) の実装をしたいと思います。実装内容は、Internet Explorer のアドインとして組み込まれ、常に私のブログへ強制的に遷移させてしまうという、非常に悪質な モジュールです(笑)。

 まず、ネイティブ 且つ COM である必要があるので、ATL ( Active Template Library ) で実装します。下図の通り、ATL Project を新規作成します。ここでは、プロジェクト名を『 BHO_Sample 』とします。( ああ、ATL とか懐かしすぎる.... )

f:id:koogucc11:20140101012457p:plain

 Finish をクリックします。昔と比較すると、随分設定がなくなった気がしますが、気のせいでしょうか?スレッド モデル、インターフェイスとかの設定がウィザードにありませんでしたっけ?

f:id:koogucc11:20131214102322p:plain

 まず、COM 開発の基本となる、インターフェース定義を行います。インターフェースの定義は、IDL ( Interface Definition Language ) を使用します。( C# とはかなり勝手が違いますね。くじけずに読んでください。 ) プロジェクト中に、BHO_Sample.idl というファイルが存在します。内容は下記の通りです。ATL の各種設定は、IDL ベースでしか出来なくなったのでしょうか?

// BHO_Sample.idl : IDL source for BHO_Sample
//

// This file will be processed by the MIDL tool to
// produce the type library (BHO_Sample.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";

[
	uuid(DBBF27F7-A2EE-46B5-A13C-6193BD0519C2),
	version(1.0),
]
library BHO_SampleLib
{
	importlib("stdole2.tlb");
};

 import 宣言配下に、IDL を利用してインターフェースを定義します。uuid は、Visual Studio の GUID ツールなどを使用して生成してください。

// BHO_Sample.idl : IDL source for BHO_Sample
//

// This file will be processed by the MIDL tool to
// produce the type library (BHO_Sample.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";
[
	object,
	uuid(40BE7058-278F-4B78-9C8C-9FDBDCBC5495),
	dual,
	nonextensible,
	helpstring("BHO インターフェース"),
	pointer_default(unique)
]
interface IBHOSample : IDispatch{
};
[
	uuid(06263513-A9E9-41BA-BF18-686378B887D3),
	version(1.0),
]
library BHO_SampleLib
{
	importlib("stdole2.tlb");
	[
		uuid(0E22B494-B0E4-4679-961B-B835CE873488)
	]
	coclass BHOSample
	{
		[default] interface IBHOSample;
	};
};

 上記のインターフェースを Visual Studio 上からコンパイルします。作成した IDL ファイルを右クリックし、コンパイルを選択します。 (実際には、MIDL でコンパイルしています。MIDL に関してはここを参照してください。) コンパイルが成功すると、BHO_Sample_i.c および BHO_Sample_i.h が生成されます。これらのファイルは、BHO 実装時に触ることはなく、 include で使用するだけです。IDL をコンパイルしたら、こんなものが生成されるんだなという認識程度で十分です(だと思います)。

※BHO_Sample_i.c

/* this ALWAYS GENERATED file contains the IIDs and CLSIDs */

/* link this file in with the server and any clients */


 /* File created by MIDL compiler version 8.00.0603 */
/* at Thu Jan 02 05:52:50 2014
 */
/* Compiler settings for BHO_Sample.idl:
    Oicf, W1, Zp8, env=Win32 (32b run), target_arch=X86 8.00.0603 
    protocol : dce , ms_ext, c_ext, robust
    error checks: allocation ref bounds_check enum stub_data 
    VC __declspec() decoration level: 
         __declspec(uuid()), __declspec(selectany), __declspec(novtable)
         DECLSPEC_UUID(), MIDL_INTERFACE()
*/
/* @@MIDL_FILE_HEADING(  ) */

#pragma warning( disable: 4049 )  /* more than 64k source lines */


#ifdef __cplusplus
extern "C"{
#endif 


#include <rpc.h>
#include <rpcndr.h>

#ifdef _MIDL_USE_GUIDDEF_

#ifndef INITGUID
#define INITGUID
#include <guiddef.h>
#undef INITGUID
#else
#include <guiddef.h>
#endif

<長いので、以下省略>

※BHO_Sample_i.h

/* this ALWAYS GENERATED file contains the IIDs and CLSIDs */

/* link this file in with the server and any clients */


 /* File created by MIDL compiler version 8.00.0603 */
/* at Thu Jan 02 05:52:50 2014
 */
/* Compiler settings for BHO_Sample.idl:
    Oicf, W1, Zp8, env=Win32 (32b run), target_arch=X86 8.00.0603 
    protocol : dce , ms_ext, c_ext, robust
    error checks: allocation ref bounds_check enum stub_data 
    VC __declspec() decoration level: 
         __declspec(uuid()), __declspec(selectany), __declspec(novtable)
         DECLSPEC_UUID(), MIDL_INTERFACE()
*/
/* @@MIDL_FILE_HEADING(  ) */

#pragma warning( disable: 4049 )  /* more than 64k source lines */


/* verify that the <rpcndr.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCNDR_H_VERSION__
#define __REQUIRED_RPCNDR_H_VERSION__ 475
#endif

#include "rpc.h"
#include "rpcndr.h"

#ifndef __RPCNDR_H_VERSION__
#error this stub requires an updated version of <rpcndr.h>
#endif // __RPCNDR_H_VERSION__

#ifndef COM_NO_WINDOWS_H
#include "windows.h"
#include "ole2.h"
#endif /*COM_NO_WINDOWS_H*/

#ifndef __BHO_Sample_i_h__
#define __BHO_Sample_i_h__

#if defined(_MSC_VER) && (_MSC_VER >= 1020)
#pragma once
#endif

/* Forward Declarations */ 

#ifndef __IBHOSample_FWD_DEFINED__
#define __IBHOSample_FWD_DEFINED__
typedef interface IBHOSample IBHOSample;

#endif 	/* __IBHOSample_FWD_DEFINED__ */


#ifndef __BHOSample_FWD_DEFINED__
#define __BHOSample_FWD_DEFINED__

#ifdef __cplusplus
typedef class BHOSample BHOSample;
#else
typedef struct BHOSample BHOSample;
#endif /* __cplusplus */
<長いので、以下省略>

 これで、BHO の実装準備が完了しました。次に、BHO のメイン部分の実装をします。プロジェクトにクラスの追加をします。下図の画面で Add ボタンをクリックします。

f:id:koogucc11:20140101013435p:plain

 Class name に CBHOSample と入力し 、Finish をクリックします。.h file および .cpp file は 自動入力されます。

f:id:koogucc11:20140101013751p:plain

 BHOSample.h を下記のように書き換えます。(前回のブログで紹介した、各ページのサンプルをみながら実装しました。)  ここ( ATL COM オブジェクトの基本事項)を参照するとわかりやすいです。BHO では、さらに IObjectWithSiteImpl が非常に重要で、ブラウザとやりとりをするための、重要なメソッドが用意されています。

#pragma once

#include "resource.h"      // メイン シンボル
#include "BHO_Sample_i.h"  // IDLから生成された情報
#include <shlguid.h>       // IID_IWebBrowser2、DIID_DWebBrowserEvents2
#include <exdispid.h>      // DISPID_DOCUMENTCOMPLETE

using namespace ATL;

// IDispatchImplなどから派生したものを作成する
class ATL_NO_VTABLE CBHOSample :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CBHOSample, &CLSID_BHOSample>,
	public IObjectWithSiteImpl<CBHOSample>,
	public IDispatchImpl<IBHOSample, &IID_IBHOSample, &LIBID_BHO_SampleLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
	public IDispEventImpl<1, CBHOSample, &DIID_DWebBrowserEvents2, &LIBID_SHDocVw, 1, 1>
{
public:
	CBHOSample()
	{
	}

	// レジストリ登録の ATL マクロ
	DECLARE_REGISTRY_RESOURCEID(IDR_BHO_SAMPLE)

	// スレッドプールからオブジェクトを生成するため?
	DECLARE_NOT_AGGREGATABLE(CBHOSample)

	// COMのインターフェースマップの ATL マクロ
	BEGIN_COM_MAP(CBHOSample)
		COM_INTERFACE_ENTRY(IBHOSample)
		COM_INTERFACE_ENTRY(IDispatch)
		COM_INTERFACE_ENTRY(IObjectWithSite)
	END_COM_MAP()

	// イベントシンクマップの ATL マクロ
	BEGIN_SINK_MAP(CBHOSample)
		SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2, DISPID_BEFORENAVIGATE2, OnBeforeNavigate2)
	END_SINK_MAP()

	// 参照カウントが 0 になってもオブジェクトを削除されないようにする
	DECLARE_PROTECT_FINAL_CONSTRUCT()

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

	// イベントシンクの登録および解除
	STDMETHOD(SetSite)(IUnknown *pUnkSite);

	// DWebBrowserEvents2 ハンドラー
	void STDMETHODCALLTYPE OnBeforeNavigate2(IDispatch* pDisp, VARIANT* url, VARIANT* Flags, VARIANT* TargetFrameName, VARIANT* PostData, VARIANT* Headers, VARIANT_BOOL* Cancel);

private:
	CComPtr<IWebBrowser2>  m_spWebBrowser; // IWebBrowser2(ブラウザのコントロール) へのポインタをキャッシュ
	BOOL m_fAdvised;

};

OBJECT_ENTRY_AUTO(__uuidof(BHOSample), CBHOSample)

 BHOSample.cpp を下記のように書き換えます。 SetSite および OnBeforeNavigate2 の実装を行います。

#include "stdafx.h"
#include "BHOSample.h"

// SetSite の実装をします。
STDMETHODIMP CBHOSample::SetSite(IUnknown* pUnkSite)
{
	if (pUnkSite)
	{
		// Internet Explorer 起動時の処理を記述します。
		// IWebBrowser2(ブラウザのコントロール) へのポインタをキャッシュしておきます。
		HRESULT hr = pUnkSite->QueryInterface(IID_IWebBrowser2, (void**)&m_spWebBrowser);
		if (SUCCEEDED(hr))
		{
			// DWebBrowserEvents2 からのイベントシンクに登録します。
			hr = DispEventAdvise(m_spWebBrowser);
			if (SUCCEEDED(hr))
			{
				m_fAdvised = TRUE;
			}
		}
	}
	else
	{
		// Internet Explorer 終了時の処理を記述します。
		// イベントシンクを解除します。
		if (m_fAdvised)
		{
			DispEventUnadvise(m_spWebBrowser);
			m_fAdvised = FALSE;
		}
		// リソースを解放します。
		if (m_spWebBrowser) m_spWebBrowser.Release();
	}

	// 基本クラスに戻ります。
	return IObjectWithSiteImpl<CBHOSample>::SetSite(pUnkSite);
}

// ブラウザでページを参照すると、常に私のブログ (http://ryuchan.hatenablog.com/) に誘導します。
void STDMETHODCALLTYPE CBHOSample::OnBeforeNavigate2(IDispatch* pDisp, VARIANT* url, VARIANT* Flags, VARIANT* TargetFrameName, VARIANT* PostData, VARIANT* Headers, VARIANT_BOOL* Cancel)
{
	// 私のブログの場合は何もしません。
	if (_tcsstr(url->bstrVal, _T("http://ryuchan.hatenablog.com/")))
	{
		// 今は何もしない。
	}
	// 私以外のサイトであれば、強制的に http://ryuchan.hatenablog.com/ へ遷移します。
	else
	{
		CComPtr<IWebBrowser2> spWebBrowser;
		HRESULT hr = pDisp->QueryInterface(IID_IWebBrowser2, reinterpret_cast<void**>(&spWebBrowser));
		if (SUCCEEDED(hr))
		{
			*Cancel = VARIANT_TRUE;
			CComVariant spVar;

			MessageBox(0, L"http://ryuchan.hatenablog.com/に遷移します。", L"http://ryuchan.hatenablog.com/", MB_OK);

			spWebBrowser->Stop();
			spWebBrowser->Navigate(_T("http://ryuchan.hatenablog.com/"), &spVar, &spVar, &spVar, &spVar);
			spWebBrowser.Release();
		}
	}
}

 これで BHO の実装は完了です。C# になれてしまうと、C++ & ATL の実装は大変だなぁと思ってしまいます。.NETFramework での開発は本当に楽ですね。

 最後に、生成されたアセンブリレジストリに登録するため、rgs ファイル (レジストリ登録のための設定ファイル)を記述します。プロジェクト内に、BHO_Sample.rgs がありますので、そのファイルを下記のように変更します。各 uuid は BHO_Sample.idl の情報を元に設定をしてください。( .NETFramework だとレジストリの設定なんて意識しないもんなぁ。 )

HKCR
{
	NoRemove CLSID
	{
		ForceRemove {0E22B494-B0E4-4679-961B-B835CE873488} = s 'BHOSample Class'
		{
			ForceRemove Programmable
			InprocServer32 = s '%MODULE%'
			{
				val ThreadingModel = s 'Apartment'
			}
			TypeLib = s '{06263513-A9E9-41BA-BF18-686378B887D3}'
			Version = s '1.0'
		}
	}
}

HKLM
{
	NoRemove SOFTWARE
	{
		NoRemove Microsoft
		{   
			NoRemove Windows
			{
				NoRemove CurrentVersion 
				{
					NoRemove Explorer 
					{
						NoRemove 'Browser Helper Objects' 
						{
							ForceRemove '{0E22B494-B0E4-4679-961B-B835CE873488}' = s 'BHOSample' 
							{
								val 'NoExplorer' = d '1'
							}
						}
					}
				}
			}
		}
	}
}

 これで、今回の実装・設定は完了です。 ソリューションをビルドすると、BHO_Sample.dll が生成されます。

 BHO_Sample.dll を BHO として Internet Explorer に認識させるには、コマンドプロンプトで下記のコマンドを実行します。

regsvr32 BHO_Sample.dll

 BHO_Sample.rgs の定義通りに、レジストリが登録されます。レジストリエディタを使用すると、CLSID、インターフェース、ブラウザヘルパーオブジェクト が登録されているのがわかります。

f:id:koogucc11:20140102222559p:plain

f:id:koogucc11:20140102222615p:plain

f:id:koogucc11:20140102222627p:plain

 それでは、動作確認をしてみましょう。インターネットエクスプローラーを起動します。インターネットエクスプローラー起動直後、下部にメッセージが表示されますので、『有効にする』をクリックします。

f:id:koogucc11:20140102144232p:plain

 ツール → アドイン管理 をクリックします。

f:id:koogucc11:20140102144141p:plain

 今回作成した BHOSample Class が登録されています。

f:id:koogucc11:20140103115119p:plain

 有効化された BHO を実際に動作させるには、インターネットエクスプローラーを再起動する必要があります。インターネットエクスプローラーを再起動すると下記のようなメッセージが表示されます。OK をクリックすると、私のブログへ強制的に遷移します。

f:id:koogucc11:20140102144336p:plain

f:id:koogucc11:20140102144554p:plain

 このままでは、まともなブラウジングができないので、BHO のレジストリを解除しておきましょう。コマンドプロンプトで下記のコマンドを実行します。

regsvr32 /u BHO_Sample.dll

 これで、私が実装した悪質な BHO_Sample.dll のレジストリは削除され、私のブログへ強制的に遷移されることもなくなります。以上で、BHO の実装・設定および動作確認が一通り完了しました。

 今後は、『今更感』はありますが COM の仕組みについて投稿したいと思います。( BHO  の実装より先にやったほうがよかったですね。)  このような開発をする場合には、まだまだ ネイティブの COM 開発も現役ですね。Windows 開発者であれば、覚えておいて損はない気がします。

 

 また寄り道です。全然関係ないですけど、Windows 8.1 タブレットの Lenovo Miix 2 8 いいですね。最近、Nexus 7 を購入したんですが、Windows 8.1 タブレットもほしくなりました。

 Venue 8 Pro も捨てがたい...

 dynabook はどうなんだろうか。日本勢もがんばれー。

東芝 dynabook Tab VT484/26K

東芝 dynabook Tab VT484/26K

 

 Acer もいいな。

 Windows 8.1 タブレットほしいですねぇ。すいません、また話題がそれてしまいました。