と大きな字で書くほど,大した資料ではないですが(^_^;)
と大きな字で書くほど,大した資料ではないですが(^_^;)
BeckyAx はBecky! Internet MailのPlugInであるとともに,ActiveXサーバとしての実装もあります。最初にこのアイデアを思いついた時には,「PlugIn DLLにActiveXサーバ用のCom実装を追加するだけだから,簡単だ」と思ったのですが…
ここまで書いただけで,ActiveX/COMに詳しい方なら考えの浅さに気付かれるのかも知れません。いずれにしろ,それなりの試行錯誤の結果として,BeckyAxはめでたく日の目を見ることができました。備忘録もかねて,その経過を少しまとめておきたいと思います。
なお,以下の実装では,Visual Studio 6.0 を使用しました。
Becky! Internet Mail にはPlugInをロードするためのインターフェースが定義されています。この機構は詳細にドキュメント化されている上に,SDKまで用意されているので,これに沿って機能を実装すれば,簡単にPlugInが作成できます。このSDKにはソースファイルのテンプレートまで含まれていて,まさに至れり尽くせりです。
Becky! Plugin SDKはここから入手できます。含まれているドキュメントは(なぜか)英語ですが,日本語訳がここにあります。どちらも Freeです。感謝!!
ActiveXサーバの実装も,VC++の充実したウィザードのおかげで難しいところは何もありません。BeckyAxでは,ActiveXサーバ記述のフレームとして,ATLを使用しています。
VC++のメニューからプロジェクトを新規作成し,ATL COM AppWizardを選択します。このウィザードは単純な作りで,操作は1ステップしかありません。PlugIn DLLを併用するのですから,サーバタイプとして「ダイナミックライブラリ(DLL)」を選択して,終了します。これでスケルトンが完成しました。
Becky! のPlugIn では,APIを使用する前に初期化処理が必要です。まず手始めに,これを実装することにしました。最初に,SDKに付属しているソースファイルにあるAPIクラスCBeckyAPIを継承するクラスを定義します。
class Becky : public CBeckyAPI { static Becky* this_; static CriticalSection cs_; protected: Becky() {}; public: static Becky* instance(void); virtual ~Becky() {}; };
BeckyAPIはすべて,このインスタンスを経由してコールします。このクラスはSingletonになっていて,このインスタンス取得時にAPIの初期化を行っています。
Becky* Becky::instance(void) { BOOL result = TRUE; cs_.Enter(); if(this_ == NULL) { this_ = new Becky(); result = this_->InitAPI(); } cs_.Leave(); if(result == FALSE) { throw Exception("Becky:instance", TStringUty::resourceString(IDS_E_INIT).c_str()); } return this_; }
次に,ActiveXインターフェースにメソッドを追加します。Wizardが作成してくれたIDLファイルにメソッドを追加しますが,VC++のATLサポートを利用して簡単に行うことができます。 ここでは,以下のような感じで定義しました。
[id(1), helpstring("Becky! のバージョンを取得する";)] HRESULT GetVersion([out, retval] BSTR *pVal);
メソッドの戻り値として,バージョン文字列が取得できる手順です。ActiveXサーバクラス側の実装は,先ほどのBeckyクラスを使用して簡単にできました。
STDMETHODIMP CBeckyAxSvr::GetVersion(BSTR *pVal) { try{ _bstr_t version(Becky::instance()->GetVersion()); *pVal = version.copy(); return S_OK; } catch(Exception e) { return reportError(e.getMessage()); } catch(_com_error e) { return e.Error(); } }
catchブロック内のコードは,エラーを報告するためのものです。
メソッドが1つできましたので,テストしてみることにしました。VC++からビルドし,出来上がったDLLをBecky!のプラグインフォルダにおきます。Becky!を再起動すれば,新しいPlugInのインストールを確認するダイアログが表示され,正しく認識されていることがわかります。
PlugIn側は問題ありませんでしたので,ActiveXインターフェースを確認します。一番簡単なVBを使用することにしました。VBの新しいプロジェクトを立ち上げ,ActiveX参照を追加します。"BeckyAx 1.0 タイプ ライブラリ"という名称がありますので,これを参照設定してやります。これで,VBソース内でActiveXサーバインスタンスBeckyAxSvrが使用可能になっています。
Private Sub Form_Load() Dim becky As New BeckyAxSvr Dim strVer As String strVer = becky.GetVersion Call MsgBox(strVer) End Sub
こんな感じになります。
これをコンパイルして即実行,Becky!から取得したバージョン文字列がメッセージボックスに表示…されません。考えること暫し。
理由は簡単でした。作成したActiveXサーバがDLL,すなわち,いわゆる"InProcess Server"であったためです。この形式のサーバは,参照するプロセスのメモリ空間にロードされて稼動します。つまりは,Becky!がPlugInとしてロードしてくれたコードと,VBアプリが使用したコードは,別のプロセスで動いていた,という訳です。
と理由が分かったところで,ちょっと考えてしまいました。インスタンスを共有するにはもうひとつの,いわゆる"OutProcess Server"にしておく必要があります。この形式のサーバは,これは当然ともいえますが,独立したアプリ(exeファイル)になります。PlugIn DLLにはできません。さて,どうするか?
いろいろ試行錯誤しましたが,最終的には解決策が見つかりました。それは“Becky!自体をActiveXサーバとして登録する”という,ごく当然の結論です。ただし,その方法を見つけるまでには,それなりに苦労したのですが。
VC++のATL Com Wizardで生成したActiveXサーバは,自身をサーバとして登録するのに”レジストラ”という機構を使用します。このとき登録する内容は,実行ファイルやDLLのリソースとして登録されています。VC++のプロジェクト上は,***.rgsという名前のソースとして参照することができます。
Wizardを使ってプロジェクトを生成して,OutProcess(exe)とInProcess(dll)の定義内容を比較してみると,それほど大きな違いがないことが分かりました。そこで前者を参考にして,DLL形式で生成したプロジェクトのrgs定義を書き換えてやることにしました。
基本的には,
の2点です。作成したrgsファイルは次の2つになりました。
HKCR { BeckyAx.BeckyAxSvr.1 = s 'BeckyAxSvr Class' { CLSID = s '{47DB5FD1-C191-41E9-9D8A-DA6B77C9777F}' } BeckyAx.BeckyAxSvr = s 'BeckyAxSvr Class' { CLSID = s '{47DB5FD1-C191-41E9-9D8A-DA6B77C9777F}' CurVer = s 'BeckyAx.BeckyAxSvr.1' } NoRemove CLSID { ForceRemove {47DB5FD1-C191-41E9-9D8A-DA6B77C9777F} = s 'BeckyAxSvr Class' { ProgID = s 'BeckyAx.BeckyAxSvr.1' VersionIndependentProgID = s 'BeckyAx.BeckyAxSvr' ForceRemove 'Programmable' LocalServer32 = s 'C:\Program Files\RimArts\B2\B2.exe' val AppID = s '{2F4659F2-4D2D-4C02-ABA1-0B35ED06F996}' 'TypeLib' = s '{0600EDB1-C4F4-4609-92A3-3879C99278A0}' } } }
HKCR { NoRemove AppID { {2F4659F2-4D2D-4C02-ABA1-0B35ED06F996} = s 'B2' 'B2.EXE' { val AppID = s {2F4659F2-4D2D-4C02-ABA1-0B35ED06F996} } } }
これをビルド,実行してみると…今度は期待通り,Becky!の情報を取得することができました。
一旦繋がってしまえば,他のAPIを実装するのは難しくありません。ただひたすらIDLにメソッドを定義し,実装するだけです。実際のコードは,ActiveX経由できた要求をAPIにブリッジしているだけなので,これまた楽勝に実装できます(^_~)V
ただしAPIの中には,外部プロセスから参照しても意味のない情報(Window HANDLEなど)もあります。このあたりは結構アバウトに判断し,実装範囲を決めていきました。最終的に実装したのは,次のようなメソッドです。
// API - システム [id(1), helpstring("Becky! のバージョンを取得する")] HRESULT GetVersion([out, retval] BSTR *pVal); [id(2), helpstring("指定された Becky! のコマンドを実行する")] HRESULT Command([in]BSTR command, [in, defaultvalue(BkHwndMain)]enum BkHwnd wnd); [id(3), helpstring("現在使用しているデータフォルダのフルパス名を取得する")] HRESULT GetDataFolder([out, retval] BSTR* pVal); [id(4), helpstring("現在使用している一時フォルダのフルパス名を取得する")] HRESULT GetTempFolder([out, retval] BSTR* pVal); [id(5), helpstring("対象ウィンドウのステータスバーに指定されたテキストを表示する")] HRESULT SetMessageText([in] BSTR message, [in, defaultvalue(BkHwndMain)] enum BkHwnd wnd); [id(6), helpstring("インターネットに接続するか、切断するかを指定する")] HRESULT Connect([in] BOOL bConnect); // API - メールボックス管理 [id(7), helpstring("現在のメールボックスを取得する")] HRESULT GetCurrentMailBox([out, retval] BSTR* pVal); [id(8), helpstring("現在のメールボックスを切り替える")] HRESULT SetCurrentMailBox([in] BSTR bstr); // API - フォルダ管理 [id(9), helpstring("現在のメールフォルダのパスを取得する")] HRESULT GetCurrentFolder([out, retval] BSTR* pVal); [id(10), helpstring("カレントフォルダを切り替えるr")] HRESULT SetCurrentFolder([in] BSTR folder); [id(11), helpstring("フォルダIDを表示名に変換する")] HRESULT GetFolderDisplayName([in] BSTR pSrc, [out, retval] BSTR* pVal); // API - 受信メール管理 [id(12), helpstring("現在のメールの識別子を取得する")] HRESULT GetCurrentMail([out, retval] BSTR* bstr); [id(13), helpstring("フォーカスしているメールを切りかえる")] HRESULT SetCurrentMail([in] BSTR bstr); [id(14), helpstring("次のメールを検索する")] HRESULT GetNextMail([out] BSTR* mailID, [in] LONG nStart, [in] BOOL bSelected, [out, retval] LONG* count); [id(15), helpstring("指定されたメールを選択/非選択にする")] HRESULT SetSel([in] BSTR id, [in] BOOL bSel); [id(16), helpstring("フォルダにメッセージを追加する")] HRESULT AppendMessage([in] BSTR folderID, [in] BSTR message, [out, retval] BOOL* bResult); [id(17), helpstring("未読メッセージを表示している時にSPACEキーを打鍵したときと同じような動作を行う")] HRESULT NextUnread([in] BOOL bBackScroll, [in] BOOL bGoNext, [out, retval] BOOL* bResult); [id(18), helpstring("選択されたメールを指定されたフォルダに移動する")] HRESULT MoveSelectedMessages([in] BSTR bstrFolderID, [in] BOOL bCopy, [out, retval] BOOL* bResult); [id(19), helpstring("メールの状態を取得する")] HRESULT GetStatus([in] BSTR bstrMailID, [out, retval] LONG* dwStatus); [id(20), helpstring("メールの状態を変更する")] HRESULT SetStatus([in] BSTR bstrMailID, [in] LONG dwSetStatus, [in] LONG dwResetStatus, [out, retval] LONG* dwNewStatus); [id(21), helpstring("指定されたメールのcharset情報を取得する")] HRESULT GetCharSet([in] BSTR bstrMailID, [out] BSTR* bstrCharSet, [out, retval] LONG* pCodePage); [id(22), helpstring("指定されたメールの完全なソース(未デコード)を取得する")] HRESULT GetSource([in] BSTR bstrMailID, [out, retval] BSTR* bstrSource); [id(23), helpstring("メールソースを指定されたメールに適用する")] HRESULT SetSource([in] BSTR bstrMailID, [in] BSTR bstrSource); [id(24), helpstring("指定されたメールのヘッダ部分(デコード済)を取得する")] HRESULT GetHeader([in] BSTR bstrMailID, [out, retval] BSTR* bstrHeader); [id(25), helpstring("現在のメールより指定されたヘッダフィールドを取得する")] HRESULT GetSpecifiedHeader([in] BSTR bstrHeaderField, [out, retval] BSTR* bstrValue); [id(26), helpstring("指定されたヘッダにデータを設定するr")] HRESULT SetSpecifiedHeader([in] BSTR bstrHeaderField, [in] BSTR bstrValue); [id(27), helpstring("現在のメールのテキスト部分(デコード済み)を取得する")] HRESULT GetText([out] BSTR* bstrMimeType, [out, retval] BSTR* bstrText); [id(28), helpstring("現在のメールにテキストを設定する")] HRESULT SetText([in] enum BkMailTextSetMode mode, [in] BSTR bstrText); // API - 送信メール管理 [id(29), helpstring("メール作成ウィンドウを開く")] HRESULT ComposeMail([in] BSTR bstrURL, [out, retval] DWORD* hWnd); [id(30), helpstring("送信メッセージの charset 情報を取得する")] HRESULT CompGetCharSet([in] DWORD hWnd, [out] BSTR* bstrCharSet, [out, retval] LONG* pCodePage); [id(31), helpstring("送信メッセージの完全なソーステキスト(未デコードの内容)を取得する")] HRESULT CompGetSource([in] DWORD hWnd, [out, retval] BSTR* bstrSource); [id(32), helpstring("送信メッセージにソーステキスト(未デコードの内容)を設定する")] HRESULT CompSetSource([in] DWORD hWnd, [in] BSTR bstrSource); [id(33), helpstring("送信メッセージのヘッダ部分を取得する")] HRESULT CompGetHeader([in] DWORD hWnd, [out, retval] BSTR* bstrHeader); [id(34), helpstring("現在のメールより指定されたヘッダ情報を取得する")] HRESULT CompGetSpecifiedHeader([in] DWORD hWnd, [in] BSTR bstrField, [out, retval] BSTR* bstrData); [id(35), helpstring("指定されたヘッダにデータを設定する")] HRESULT CompSetSpecifiedHeader([in] DWORD hWnd, [in] BSTR bstrField, [in] BSTR bstrData); [id(36), helpstring("送信メッセージのテキスト部分を取得する")] HRESULT CompGetText([in] DWORD hWnd, [out] BSTR* bstrMimeType, [out, retval] BSTR* bstrText); [id(37), helpstring("送信メッセージに対してテキストを設定する")] HRESULT CompSetText([in] DWORD hWnd, [in] enum BkMailTextSetMode mode, [in] BSTR bstrText); [id(38), helpstring("送信メッセージに指定ファイルを添付する")] HRESULT CompAttachFile([in] DWORD hWnd, [in] BSTR bstrFilePath, [in, optional] VARIANT vMimeType);
ここまで説明したのはBecky! APIの中で,PlugInから呼び出しを行うものについてです。これ以外にAPIには,Becky!でのイベント発生を通知するコールバックも定義されています。
これらのイベントを,ActiveXのイベントとして実装することにしました。この実装は,いわゆるdispinterfaceを使用して行います。
Becky!からのコールバックは,PlugIn インターフェースとして定義されているDLLコールを経由して行われます。ここで受けたパラメータをもとに,IDLをコンパイルしてできたスタブをコールしてやればいいのです。特に難しい部分はありません。
IDLの定義は,次のような感じで記述しました。ここでも,いくつかのAPIは端折ってあります(悪しからず)。また,周期的なコールバックのような,ActiveX経由ではあまり意義のなさそうなものも実装していません。
[ uuid(32A108CF-E060-4FF8-8C7E-4A007BF156E5), helpstring("_IBeckyAxSvrEvents Interface") ] dispinterface _IBeckyAxSvrEvents { properties: methods: [id(1), helpstring("メインウィンドウが作成されたときに呼び出される")] void OnStart(); [id(2), helpstring("メインウィンドウが閉じる直前に呼び出される")] void OnExit(); [id(3), helpstring("フォルダがオープンされるときに呼び出される")] void OnOpenFolder([in] BSTR folderID); [id(4), helpstring("メールが選択されるときに呼び出される")] void OnOpenMail([in] BSTR mailID); [id(5), helpstring("メール作成ウィンドウがオープンされるときに呼び出される")] void OnOpenCompose([in] enum BkComposeMode mode); [id(6), helpstring("作成中のメールがセーブされる時に呼び出される")] void OnOutgoing([in] short mode); [id(7), helpstring("メッセージを受信してフォルダにセーブするときに呼び出される")] void OnRetrieve([in] BSTR mailID); [id(8), helpstring("セッション中ですべてのメッセージを受信したときに呼び出される")] void OnFinishRetrieve([in] short number); };
Dispatchインターフェース経由でActiveXサーバから通知されるイベントは,クライアント側のイベント処理で受信します。VBを使う場合,インスタンス変数定義時にWithEventを指定してやります。
Private WithEvents becky As BeckyAxSvr
後はVBがタイプライブラリを参照して,適当なメソッドのスケルトンを与えてくれます。必要なのは,その中身を記述することだけです。例えば,次のような感じです。
'' '' メール受信時に Becky! からのイベントを受信する '' Private Sub becky_OnRetrieve(ByVal mailID As String) .................... End Sub
これで一通り想定の機能がそろいました。ところが,いろいろテストプログラムで試していると,うまくいっていない部分があることが分かりました。メール受信時のコールバック"OnRetrieve"だけがコールされていないようなのです。
デバッガで調べてみると,Dispatchインターフェースで生成されたスタブのメソッドFire_OnRetreive( )コールがエラーになっているようです。そこでエラーコードを確認したところ,“スタブが別スレッドからコールされている”ことが通知されていると分かりました。OnRetrieveだけは,他とは別スレッドでコールされているようです。もっともこれ自体,処理内容を想像すれば納得できるものではありますが。
根本的な問題は,使用しているCOMが"SingleThread Apartment Model"という点にあるようです。この形式の場合,Marshallerは単一スレッドからの起動しか許されておらず(しっかりチェックされているようです),別スレッドであるコールバック内での起動はできません。
Marshal関係でいろいろ調べてみたのですが,解決策としては"MultiThread Apartment Model"を使用する以外には見つかりません。このCOMモデルの指定には,WindowsAPIのCoInitializeExを使用します。
HRESULT CoInitializeEx(
void * pvReserved, //Reserved
DWORD dwCoInit //COINIT value
);
最初のパラメータはReservedで,必ずNULLを指定します。2つ目がCOM初期化値を与える部分です。今回の条件であれば,ここでCOINIT_MULTITHREADEDを指定してやればよいようです。
このAPIは,Windowsプログラム処理の最初に,1回だけコールします。通常,WinMain( )などのメイン関数で使用するのですが…。ここで問題!! 今回の場合,ActiveXサーバのメインとなるプログラムはPlugIn DLLではなく,Becky! の方です。こちらの処理を変更するわけにはいきません。
順調(でもないか)に開発を進めて来ましたが,ここで詰まってしまいました。OnRetrieveだけ諦める,という手も考えたのですが,ここまで理由を調べた以上,何とか対処したいのものです。それで,プログラム的に回避策を打つことにしました。
理屈は単純で,「Dispatch処理を行うスレッドを作っておく」というものです。Becky! API からコールバックされたメソッドでは直接処理は行わず,このワーカスレッドにメッセージを投げます。このスレッドではWindows定石のメッセージディスパッチを行って,メッセージを順次処理する,という形で実装しました。
スレッドの起動・停止は,DLLのエントリDllMain( )で行っています。DLL_PROCESS_ATTACHで起動,DLL_PROCESS_DETACHで停止,という具合です。この構造でテストしたところ,OnRetrieveを含めたコールバックを,すべてActiveXイベントとして通知することができるようになりました。
あとはインストーラとヘルプファイルが必要かな?