KB0122:安装 thinkcell 后某些 Excel COM 自动化调用失败

问题

安装 thinkcell 后,某些 Excel COM 自动化调用失败。HRESULT 错误代码为 0x8001010A RPC_E_SERVERCALL_RETRYLATER。在 LotusScript 中,错误为 Automation object error。

COM 服务器能够使用 IMessageFilter 机制过滤传入调用。更具体地说,如果 IMessageFilter::HandleInComingCall 实现返回 SERVERCALL_RETRYLATER,客户端调用将以 RPC_E_SERVERCALL_RETRYLATER 返回。

无论是否安装 thinkcell,Excel 都会使用此机制。thinkcell 的存在会增加 RPC_E_SERVERCALL_RETRYLATER 出现的频率,从而使问题更常见。

已知在这些情况下会显示错误的一个产品是 IBM Lotus Notes 的脚本引擎 LotusScript。

解决方案

响应 RPC_E_SERVERCALL_RETRYLATER 时,调用方必须短暂等待,然后重复调用。Visual Basic for Applications 内置了这种处理机制,因此如果其他支持 COM 的 Windows 脚本环境(如 LotusScript)没有执行这种处理,可以说这是一个 bug。

在脚本环境之外,或者如果脚本环境没有执行这种处理,调用方必须自行处理 RPC_E_SERVERCALL_RETRYLATER。COM 让这件事变得相当简单,因为只要告知 COM,它就可以代表客户端自动重复调用。实际的调用代码可以保持不变。

要启用此行为,客户端必须实现自己的 IMessageFilter,并让方法 IMessageFilter::RetryRejectedCall 在 dwRejectType 参数为 SERVERCALL_RETRYLATER 时返回 -1 以外的值。然后,在每个执行 COM 调用的 STA 线程中,必须使用 CoRegisterMessageFilter 将此实现的一个实例注册到 COM。

例如,下面是一个 C++ ATL RAII 实现: 阅读更多

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 实现
  DWORD STDMETHODCALLTYPE HandleInComingCall(
    /* [in] */ DWORD dwCallType,
    /* [in] */ HTASK htaskCaller,
    /* [in] */ DWORD dwTickCount,
    /* [in] */ LPINTERFACEINFO lpInterfaceInfo
  ) {
    if( m_imessagefilterOld ) {
      // 传递给旧处理程序,行为不变
      return m_imessagefilterOld->HandleInComingCall(
        dwCallType,
        htaskCaller,
        dwTickCount,
        lpInterfaceInfo
      );
    } else {
      // 默认行为
      return SERVERCALL_ISHANDLED;
    }
  }

  DWORD STDMETHODCALLTYPE RetryRejectedCall(
    /* [in] */ HTASK htaskCallee,
    /* [in] */ DWORD dwTickCount,
    /* [in] */ DWORD dwRejectType
  ) {
    if( SERVERCALL_RETRYLATER==dwRejectType ) {
      // 在脚本中,为确保负载下的稳健性而无限期等待
      return 100;
    } else if( m_imessagefilterOld ) {
      // 传递给旧处理程序,行为不变
      return m_imessagefilterOld->RetryRejectedCall(
        htaskCallee, 
        dwTickCount,
        dwRejectType );			
    } else {
      // 默认行为
      return (DWORD)-1;
    }
  }

  DWORD STDMETHODCALLTYPE MessagePending(
    /* [in] */ HTASK htaskCallee,
    /* [in] */ DWORD dwTickCount,
    /* [in] */ DWORD dwPendingType
  ) {
    if( m_imessagefilterOld ) {
      // 传递给旧处理程序,行为不变
      return m_imessagefilterOld->MessagePending(
        htaskCallee,
        dwTickCount,
        dwPendingType );
    } else {
      // 默认行为
      return PENDINGMSG_WAITDEFPROCESS;
    }
  }
};

// 在您的作用域中实例化此类,以启用稳健的 IMessageFilter。
class CHandleRetryLater :
  public CHandleRetryLaterBase
{
public:
  CHandleRetryLater() {
    // 设置新处理程序并保存旧处理程序
    _ASSERT( m_dwRef==0 );
    HRESULT hr=CoRegisterMessageFilter( this, &m_imessagefilterOld );
    // 如果之前未安装筛选器,m_imessagefilterOld 可能为 nullptr
    _ASSERT( SUCCEEDED(hr) );
  }

  ~CHandleRetryLater() {
    // 重置旧处理程序
    {
      CComPtr<IMessageFilter> iMessageFilter;
      HRESULT hr=CoRegisterMessageFilter(
        m_imessagefilterOld,
        &iMessageFilter );
      _ASSERT( SUCCEEDED(hr) );
      // 确保没有人替换我们的 IMessageFilter
      _ASSERT( iMessageFilter==static_cast<IMessageFilter*>(this) );
    } // iMessageFilter 是最后一次释放
    _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);
  }
};