BeckyAx 技術資料

と大きな字で書くほど,大した資料ではないですが(^_^;)

Contents

はじめに

BeckyAxBecky! Internet MailのPlugInであるとともに,ActiveXサーバとしての実装もあります。最初にこのアイデアを思いついた時には,「PlugIn DLLにActiveXサーバ用のCom実装を追加するだけだから,簡単だ」と思ったのですが…

ここまで書いただけで,ActiveX/COMに詳しい方なら考えの浅さに気付かれるのかも知れません。いずれにしろ,それなりの試行錯誤の結果として,BeckyAxはめでたく日の目を見ることができました。備忘録もかねて,その経過を少しまとめておきたいと思います。

なお,以下の実装では,Visual Studio 6.0 を使用しました。

Becky! PlugIn の実装

Becky! Internet Mail にはPlugInをロードするためのインターフェースが定義されています。この機構は詳細にドキュメント化されている上に,SDKまで用意されているので,これに沿って機能を実装すれば,簡単にPlugInが作成できます。このSDKにはソースファイルのテンプレートまで含まれていて,まさに至れり尽くせりです。

Becky! Plugin SDKはここから入手できます。含まれているドキュメントは(なぜか)英語ですが,日本語訳がここにあります。どちらも Freeです。感謝!!

ActiveXサーバの実装

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_;
} 

API呼び出しのブリッジ処理

次に,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);

コールバックとActiveXイベント

ここまで説明したのは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イベントとして通知することができるようになりました。

ToDo

あとはインストーラとヘルプファイルが必要かな?