ナレッジ ベース KB0122

think-cellを使用すると一部のExcel COMオートメーション呼び出しが失敗する

問題

think-cellをインストールすると、一部のExcel COMオートメーション呼び出しが失敗します。HRESULTエラー コードは、0x8001010A RPC_E_SERVERCALL_RETRYLATERです。LotusScriptでは、エラーはオートメーション オブジェクト エラーです。

COMサーバーは、IMessageFilterメカニズムを利用して着信をフィルタリングできます。具体的に言えば、IMessageFilter::HandleInComingCall実装がSERVERCALL_RETRYLATERを返すと、クライアントの呼び出しはRPC_E_SERVERCALL_RETRYLATERで返します。

Excelはthink-cellの有無に関係なく、このメカニズムを使用します。think-cellがあれば、RPC_E_SERVERCALL_RETRYLATERの頻度が増え、問題がより頻繁に発生します。

このような状況でエラーが生じることがわかっている製品のひとつが、IBM Lotus Notesのスクリプト エンジン、LotusScriptです。

解決策

RPC_E_SERVERCALL_RETRYLATERへの対応で、発信者は短時間待ってから呼び出しを繰り返す必要があります。Visual Basic for Applicationsは、この対応が内蔵されているため、COMをサポートする他のWindowsスクリプト環境(LotusScriptなど)が対応できない場合はバグになる可能性があります。

スクリプト環境の外では、またはスクリプト環境が対応しない場合は、発信者がRPC_E_SERVERCALL_RETRYLATERに自ら対応しなくてはなりません。COMでは、これがかなり容易になります。指示があれば、クライアントの代わりに呼び出しが自動的に繰り返されるためです。実際の呼び出し側コードは変更なしのままにできます。

この動作を有効にするには、クライアントがIMessageFilter::RetryRejectedCallを使用して「-1」以外を返し(dwRejectTypeパラメータがSERVERCALL_RETRYLATERの場合)、独自のIMessageFilterを実装する必要があります。その後、COM呼び出しを行う各STAスレッドCoRegisterMessageFilterを使用して、COMによるこの実装のインスタンスを登録しなくてはなりません。

たとえば、C++ ATLRAIIの実装は以下のようになります。

class CHandleRetryLaterBase :
  public CComObjectRootEx<CComSingleThreadModel>,
  public IMessageFilter
{
protected:
  CComPtr<IMessageFilter> m_imessagefilterOld;

  BEGIN_COM_MAP(CHandleRetryLaterBase)
    COM_INTERFACE_ENTRY(IMessageFilter)
  END_COM_MAP()

public:
  // IMessageFilter implementation
  DWORD STDMETHODCALLTYPE HandleInComingCall(
    /* [in] */ DWORD dwCallType,
    /* [in] */ HTASK htaskCaller,
    /* [in] */ DWORD dwTickCount,
    /* [in] */ LPINTERFACEINFO lpInterfaceInfo
  ) {
    if( m_imessagefilterOld ) {
      // pass on to old handler, no change in behavior
      return m_imessagefilterOld->HandleInComingCall(
        dwCallType,
        htaskCaller,
        dwTickCount,
        lpInterfaceInfo
      );
    } else {
      // default behavior
      return SERVERCALL_ISHANDLED;
    }
  }

  DWORD STDMETHODCALLTYPE RetryRejectedCall(
    /* [in] */ HTASK htaskCallee,
    /* [in] */ DWORD dwTickCount,
    /* [in] */ DWORD dwRejectType
  ) {
    if( SERVERCALL_RETRYLATER==dwRejectType ) {
      // in a script, wait indefinitely for robustness under load
      return 100;
    } else if( m_imessagefilterOld ) {
      // pass on to old handler, no change in behavior
      return m_imessagefilterOld->RetryRejectedCall(
        htaskCallee, 
        dwTickCount,
        dwRejectType );			
    } else {
      // default behavior
      return (DWORD)-1;
    }
  }

  DWORD STDMETHODCALLTYPE MessagePending(
    /* [in] */ HTASK htaskCallee,
    /* [in] */ DWORD dwTickCount,
    /* [in] */ DWORD dwPendingType
  ) {
    if( m_imessagefilterOld ) {
      // pass on to old handler, no change in behavior
      return m_imessagefilterOld->MessagePending(
        htaskCallee,
        dwTickCount,
        dwPendingType );
    } else {
      // default behavior
      return PENDINGMSG_WAITDEFPROCESS;
    }
  }
};

// Instantiate this class in your scope to enable robust IMessageFilter.
class CHandleRetryLater :
  public CHandleRetryLaterBase
{
public:
  CHandleRetryLater() {
    // set new handler and save old one
    _ASSERT( m_dwRef==0 );
    HRESULT hr=CoRegisterMessageFilter( this, &m_imessagefilterOld );
    // m_imessagefilterOld may be nullptr if there is no
    // filter previously installed
    _ASSERT( SUCCEEDED(hr) );
  }

  ~CHandleRetryLater() {
    // reset old handler
    {
      CComPtr<IMessageFilter> iMessageFilter;
      HRESULT hr=CoRegisterMessageFilter(
        m_imessagefilterOld,
        &iMessageFilter );
      _ASSERT( SUCCEEDED(hr) );
      // make sure noone replaced our IMessageFilter
      _ASSERT( iMessageFilter==static_cast<IMessageFilter*>(this) );
    } // iMessageFilter is last release
    _ASSERT( m_dwRef==0 );
  }

  STDMETHOD_(ULONG, AddRef)() throw() {
#ifdef _DEBUG
    return InternalAddRef();
#else
    return 0;
#endif
  }

  STDMETHOD_(ULONG, Release)() throw() {
#ifdef _DEBUG
    return InternalRelease();
#else
    return 0;
#endif
  }

  STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) throw() {
    return _InternalQueryInterface(iid, ppvObject);
  }
};