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