ExcelのRTDサーバーを、VC++で書いてみた。

12/30追記:interface IControllerのidl定義が抜けていたのを追加。

ref: ExcelのRTD用DCOMサーバの作り方 - しがないプログラマ の日記
昔にVBででっち上げたCOMサーバーを、VC++で書き換えてみようと思い立ち挑戦してみました。この辺りの情報は、英語ではある程度見つかるんですが日本語の情報に乏しいので色々とはまりました。。。

以下は、とりあえずRTDサーバーとしては簡単に渡された値を返すだけのものの作り方です。
ExcelからはRTDワークシート関数から「=RTD("RtdServerSample.Controller","","test value")」と呼び出せます。

プロジェクトの作成

「RtdServerSample」という名前で、VisualStudioのATLプロジェクトを作成します*1。オプションで作成するファイルの種類をDLLから普通のExeに変更します。デフォルトでプロジェクト名がCOMとしての名前になるため、注意が必要です。

COMクラスの作成

「Controller」という名前で、ATLシンプルオブジェクトを作成します。スレッドの種類はアパートメントにします。

idlファイルの編集

RtdServerSample.idlが生成されているので、以下の記述をimport文の後に追加する。
※下記のuuidは Excel2003向けです。他のバージョン用にする場合には Excelのタイプライブラリから「IRTDUpdateEvent 」と「IRtdServer」の定義をコピーしてくる必要があります。

[
  uuid(A43788C1-D91B-11D3-8F39-00C04F3651B8),
  dual,
  oleautomation
]
interface IRTDUpdateEvent : IDispatch {
    [id(0x0000000a)] HRESULT UpdateNotify();
    [id(0x0000000b), propget] HRESULT HeartbeatInterval([out, retval] long* value);
    [id(0x0000000b), propput] HRESULT HeartbeatInterval([in] long value);
    [id(0x0000000c)] HRESULT Disconnect();
};

[
  uuid(EC0E6191-DB51-11D3-8F3E-00C04F3651B8),
  dual,
  oleautomation
]
interface IRtdServer : IDispatch {
    [id(0x0000000a)]
    HRESULT ServerStart([in] IRTDUpdateEvent* callback,
                        [out, retval] long* result);
    [id(0x0000000b)]
    HRESULT ConnectData([in] long topicId,
                        [in] SAFEARRAY(VARIANT)* strings,
                        [in, out] VARIANT_BOOL* newValues,
                        [out, retval] VARIANT* values);
    [id(0x0000000c)]
    HRESULT RefreshData([in, out] long* topicCount,
                        [out, retval] SAFEARRAY(VARIANT)* data);
    [id(0x0000000d)]
    HRESULT DisconnectData([in] long topicId);
    [id(0x0000000e)]
    HRESULT Heartbeat([out, retval] long* result);
    [id(0x0000000f)]
    HRESULT ServerTerminate();
};

interface IController の定義に IRtdServerのメソッドを追加する。

	interface IController : IDispatch{
		[id(0x0000000a), helpcontext(0x0007a125)]
		HRESULT ServerStart(
						[in] IRTDUpdateEvent* CallbackObject, 
						[out, retval] long* pfRes);
		[id(0x0000000b), helpcontext(0x0007a126)]
		HRESULT ConnectData(
						[in] long TopicID, 
						[in] SAFEARRAY(VARIANT)* Strings, 
						[in, out] VARIANT_BOOL* GetNewValues, 
						[out, retval] VARIANT* pvarOut);
		[id(0x0000000c), helpcontext(0x0007a127)]
		HRESULT RefreshData(
						[in, out] long* TopicCount, 
						[out, retval] SAFEARRAY(VARIANT)* parrayOut);
		[id(0x0000000d), helpcontext(0x0007a128)]
		HRESULT DisconnectData([in] long TopicID);
		[id(0x0000000e), helpcontext(0x0007a129)]
		HRESULT Heartbeat([out, retval] long* pfRes);
		[id(0x0000000f), helpcontext(0x0007a12a)]
		HRESULT ServerTerminate();
	};

さらに、idlファイル内に coclass Controller の定義があるので以下の1文を追加する。

interface IRtdServer;

ここまでの作業が終わったら、一旦ビルドを行い問題ないことを確認する。

Controllerクラスの実装

Controller.hを開き以下の作業を行う。

継承クラスに以下を追加する。

public IDispatchImpl<IRtdServer, &__uuidof(IRtdServer)>

BEGIN_COM_MAP〜END_COM_MAPの間に以下を追加する*2

//COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY2(IDispatch, IRtdServer)
COM_INTERFACE_ENTRY(IRtdServer)

継承したメソッドの定義を追加するため、クラス定義の末尾に以下の記述を追加する。

private:
	IRTDUpdateEvent* m_pCallback;

// IRtdServer Methods
public:
	STDMETHOD(ServerStart)(IRTDUpdateEvent * CallbackObject, long * pfRes);
	STDMETHOD(ConnectData)(long TopicID, SAFEARRAY * * Strings, VARIANT_BOOL * GetNewValues, VARIANT * pvarOut);
	STDMETHOD(RefreshData)(long * TopicCount, SAFEARRAY * * parrayOut);
	STDMETHOD(DisconnectData)(long TopicID);
	STDMETHOD(Heartbeat)(long * pfRes);
	STDMETHOD(ServerTerminate)();

最後に、Controller.cppを開き以下の実装を追加する。

#include <atlsafe.h>

// CController

STDMETHODIMP CController::ServerStart(IRTDUpdateEvent* CallbackObject, long* pfRes)
{
	m_pCallback = CallbackObject;
	*pfRes = 1;
	return S_OK;
}

STDMETHODIMP CController::ServerTerminate()
{
	m_pCallback = NULL;
	return S_OK;
}


STDMETHODIMP CController::ConnectData(long TopicID, SAFEARRAY** Strings, VARIANT_BOOL* GetNewValues, VARIANT* pvarOut)
{
	CComBSTR result;

	CComSafeArray<VARIANT> input(*Strings);
	for (long index = input.GetLowerBound(), last = input.GetUpperBound(); index <= last; ++index) {
		const CComVariant& value = input.GetAt(index);
		value.CopyTo(&result);
	}

	CComVariant variant(result);
	HRESULT hr = variant.Detach(pvarOut);
	if ( FAILED(hr) ) {
		return E_FAIL;
	}
	return S_OK;
}

STDMETHODIMP CController::DisconnectData(long TopicID)
{
	return S_OK;
}

STDMETHODIMP CController::RefreshData(long* TopicCount, SAFEARRAY** parrayOut)
{
	CComSafeArray<VARIANT> result;
	*TopicCount = result.GetCount();
	*parrayOut = result.Detach();
	return S_OK;
}

STDMETHODIMP CController::Heartbeat(long* pfRes)
{
	*pfRes = 1;
	return S_OK;
}

以上で実装は完了です。ビルドを行えば RTDサーバーとして動きます。
後は、Controllerの実装を変えていけば、色々な値を返すサーバーを作ることができるかと思います。

*1:ExpressEditionではATLのサポートがないため、上位のEditionが必要になります

*2:コメントアウトしてある行は、既に存在するはずなのでコメントアウトして下さい。