错误与异常处理---组件
1.简单返回HRESULT
对于比较简单的错误,直接返回表示错误原因的 HRESULT。
2.抛出COM异常---调用Error(...)
既然 COM 是靠各种各样的接口来提供服务的,于是很自然地就会想到,是否有一个接口能够提供更丰富的错误信息报告那?答案是:IErrorInfo(调用SetErrorInfo(0, pErrorInfo);)。
ATL 把SetErrorInfo包装成 CComCoClass::Error() 的6个重载函数了。
函数调用过程如下:
Error --> AtlReportError --> AtlSetErrorInfo --> SetErrorInfo(0, pErrorInfo);
class CComCoClass
{
public:
DECLARE_CLASSFACTORY()
DECLARE_AGGREGATABLE(T)
typedef T _CoClass;
static const CLSID& WINAPI GetObjectCLSID() {return *pclsid;}
static LPCTSTR WINAPI GetObjectDescription() {return NULL;}
static HRESULT WINAPI Error(LPCOLESTR lpszDesc, <=== Error
const IID& iid = GUID_NULL, HRESULT hRes = 0)
{
return AtlReportError(GetObjectCLSID(), lpszDesc, iid, hRes);
}
static HRESULT WINAPI Error(LPCOLESTR lpszDesc, DWORD dwHelpID, <=== Error
LPCOLESTR lpszHelpFile, const IID& iid = GUID_NULL, HRESULT hRes = 0)
{
return AtlReportError(GetObjectCLSID(), lpszDesc, dwHelpID, lpszHelpFile,
iid, hRes);
}
static HRESULT WINAPI Error(UINT nID, const IID& iid = GUID_NULL, <=== Error
HRESULT hRes = 0, HINSTANCE hInst = _AtlBaseModule.GetResourceInstance())
{
return AtlReportError(GetObjectCLSID(), nID, iid, hRes, hInst);
}
static HRESULT WINAPI Error(UINT nID, DWORD dwHelpID,
LPCOLESTR lpszHelpFile, const IID& iid = GUID_NULL,
HRESULT hRes = 0, HINSTANCE hInst = _AtlBaseModule.GetResourceInstance()) <=== Error
{
return AtlReportError(GetObjectCLSID(), nID, dwHelpID, lpszHelpFile,
iid, hRes, hInst);
}
static HRESULT WINAPI Error(LPCSTR lpszDesc,
const IID& iid = GUID_NULL, HRESULT hRes = 0) <=== Error
{
return AtlReportError(GetObjectCLSID(), lpszDesc, iid, hRes);
}
static HRESULT WINAPI Error(LPCSTR lpszDesc, DWORD dwHelpID, <=== Error
LPCSTR lpszHelpFile, const IID& iid = GUID_NULL, HRESULT hRes = 0)
{
return AtlReportError(GetObjectCLSID(), lpszDesc, dwHelpID,
lpszHelpFile, iid, hRes);
}
...
};
--> AtlReportError:
const IID& iid = GUID_NULL, HRESULT hRes = 0)
{
return AtlSetErrorInfo(clsid, lpszDesc, 0, NULL, iid, hRes, NULL);
}
--> AtlSetErrorInfo:
LPCOLESTR lpszHelpFile, const IID& iid, HRESULT hRes, HINSTANCE hInst)
{
USES_CONVERSION;
TCHAR szDesc[1024];
szDesc[0] = NULL;
// For a valid HRESULT the id should be in the range [0x0200, 0xffff]
if (IS_INTRESOURCE(lpszDesc)) //id
{
UINT nID = LOWORD((DWORD_PTR)lpszDesc);
ATLASSERT((nID > = 0x0200 && nID <= 0xffff) ¦ ¦ hRes != 0);
if (LoadString(hInst, nID, szDesc, 1024) == 0)
{
ATLASSERT(FALSE);
lstrcpy(szDesc, _T( "Unknown Error "));
}
lpszDesc = T2OLE(szDesc);
if (hRes == 0)
hRes = MAKE_HRESULT(3, FACILITY_ITF, nID);
}
CComPtr <ICreateErrorInfo> pICEI;
if (SUCCEEDED(CreateErrorInfo(&pICEI)))
{
CComPtr <IErrorInfo> pErrorInfo;
pICEI-> SetGUID(iid);
LPOLESTR lpsz;
ProgIDFromCLSID(clsid, &lpsz);
if (lpsz != NULL)
pICEI-> SetSource(lpsz);
if (dwHelpID != 0 && lpszHelpFile != NULL)
{
pICEI-> SetHelpContext(dwHelpID);
pICEI-> SetHelpFile(const_cast <LPOLESTR> (lpszHelpFile));
}
CoTaskMemFree(lpsz);
pICEI-> SetDescription((LPOLESTR)lpszDesc);
if (SUCCEEDED(pICEI-> QueryInterface(__uuidof(IErrorInfo), (void**)&pErrorInfo)))
SetErrorInfo(0, pErrorInfo); <====== 抛出COM异常
}
return (hRes == 0) ? DISP_E_EXCEPTION : hRes;
}
最终,通过SetErrorInfo抛出COM异常
二、错误与异常处理--客户端
客户端接收组件的错误信息,有两个方式
1.返回HRESULT
2.截获COM异常--- GetErrorInfo()
而截获COM异常,也有两个方式:
2.1 使用 API 方式调用组件
if( FAILED( hr ) ) // 如果发生了错误
{
CComQIPtr < ISupportErrorInfo > spSEI = spXXX; // 组件是否提供了 ISupportErrorInfo 接口?
if( spSEI ) // 如果支持,那么
{
hr = spSEI-> InterfaceSupportsErrorInfo( IID_Ixxx ); // 是否支持 Ixxx 接口的错误处理?
if( SUCCEEDED( hr ) )
{ // 支持,太好了。取出错误信息
CComQIPtr < IErrorInfo > spErrInfo; // 声明 IErrorInfo 接口
hr = ::GetErrorInfo( 0, &spErrInfo ); <======== 截获COM异常
if( SUCCEEDED( hr ) )
{
CComBSTR bstrDes;
spErrInfo-> GetDescription( &bstrDes ); // 取得错误描述
...... // 还可以取得其它的信息
2.2 使用 #import 等包装方式调用组件,然后抛出C++异常,再然后由客户端截获
1.使用 #import 等包装组件
比如:
为什么可以使用try/catch结构就可以截获COM异常呢?
因为,当你使用 #import 引入组件类型库后,编译器帮你包装为一个接口的C++类,而成员函数中,使用了throw,所以你就能catch了。具体它如何包装的,你编译后,打开 tlh 文件去看。
如:
VARIANT _result;
VariantInit(&_result);
HRESULT _hr = get_Value(Name, &_result);
if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this)); <==抛出C++异常
return _variant_t(_result, false);
}
其中:
void __stdcall _com_issue_errorex(HRESULT, IUnknown*, REFIID) throw(_com_error);
_com_issue_errorex的实现MS没有开放源码,但应该就是使用COM异常信息来填充_com_error信息(比如m_hresult, IErrorInfo* 等),然后抛出C++异常。
*注意:_com_issue_errorex是如何巧妙实现COM异常到C++异常转换的。
3. 客户端截获C++异常
try
{
...... // 调用组件功能
}
catch( _com_error &e )
{
e.Description(); // 取得错误描述信息
...... // 还可以调用 _com_error 函数取得其它信息
}
_com_error的定义如下:
public:
// Constructors
//
_com_error(HRESULT hr,
IErrorInfo* perrinfo = NULL,
bool fAddRef = false) throw();
_com_error(const _com_error& that) throw();
// Destructor
//
virtual ~_com_error() throw();
// Assignment operator
//
_com_error& operator=(const _com_error& that) throw();
// Accessors
//
HRESULT Error() const throw();
WORD WCode() const throw();
IErrorInfo * ErrorInfo() const throw();
// IErrorInfo method accessors
//
_bstr_t Description() const throw(_com_error);
DWORD HelpContext() const throw();
_bstr_t HelpFile() const throw(_com_error);
_bstr_t Source() const throw(_com_error);
GUID GUID() const throw();
// FormatMessage accessors
const TCHAR * ErrorMessage() const throw();
// EXCEPINFO.wCode <-> HRESULT mappers
static HRESULT WCodeToHRESULT(WORD wCode) throw();
static WORD HRESULTToWCode(HRESULT hr) throw();
private:
enum {
WCODE_HRESULT_FIRST = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x200),
WCODE_HRESULT_LAST = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF+1, 0) - 1
};
const HRESULT m_hresult;
IErrorInfo * m_perrinfo;
mutable TCHAR * m_pszMsg;
};
*注意:其实_com_error就是对IErrorInfo的包装,用截获的COM异常来填充;
总之,客户端接收组件错误信息就两种方式
1。返回HRESULT
2。组件:SetErrorInfo(..., IErrorInfo *) 抛出COM异常
客户端:GetErrorInfo(..., IErrorInfo *) 截获COM异常
而客户端之所以可以截获C++异常,主要是应为编译时对“返回HRESULT”和GetErrorInfo进行了包装。
如果不对返回错误信息进行C++异常包装(_com_error)然后抛出,会很繁的。
就像这样,处理每个可能的错误,都要一大串代码 ---晕!
if( FAILED( hr ) ) // 如果发生了错误
{
CComQIPtr < ISupportErrorInfo > spSEI = spXXX; // 组件是否提供了 ISupportErrorInfo 接口?
if( spSEI ) // 如果支持,那么
{
hr = spSEI-> InterfaceSupportsErrorInfo( IID_Ixxx ); // 是否支持 Ixxx 接口的错误处理?
if( SUCCEEDED( hr ) )
{ // 支持,太好了。取出错误信息
CComQIPtr < IErrorInfo > spErrInfo; // 声明 IErrorInfo 接口
hr = ::GetErrorInfo( 0, &spErrInfo ); <======== 截获COM异常
if( SUCCEEDED( hr ) )
{
CComBSTR bstrDes;
spErrInfo-> GetDescription( &bstrDes ); // 取得错误描述
...... // 还可以取得其它的信息
还好,微软聪明的工程师帮我们通过C++异常的方式,很巧妙地处理了这个问题。
使得程序变得如此简明!^_^
try
{
spXXX-> fun() // 调用组件功能
}
catch( _com_error &e )
{
e.Description(); // 取得错误描述信息
...... // 还可以调用 _com_error 函数取得其它信息
}
对组件错误的处理,根据返回HRESULT,可以获得基本错误信息。
如果你认为足够了,OK,这就行了。
但如果你要向错误信息中加入其它一些信息,这时就需要抛出COM异常---调用Error(...)。
然后由客户端截获异常,获得额外的信息。
其实在客户端可以得到的信息就是:HRESULT(_hr)和GetErrorInfo
经MS包装后,抛出异常的过程是这样的:
if (FAILED(_hr){
使用GetErrorInfo得到的信息,来填充_com_error
然后抛出 _com_error
}
这些都是编译器作的手脚。十分巧妙!
PF!
另外,使用 #import 等包装组件,组件有些方法通过返回值来传递[out, retval]参数,这样就不会返回HRESULT。 所以,当使用 #import 等包装组件时,必须通过try{}catch{},来捕获错误。
对于支持抛出COM异常的组件,需要ISupportErrorInfo接口
public CComObjectRootEx <CComSingleThreadModel> ,
public CComCoClass <CParser, &CLSID_Parser> ,
public ISupportErrorInfo,
public IDispatchImpl <IParser, &IID_IParser, &LIBID_sampleLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
组件的ISupportErrorInfo接口其实就一个方法InterfaceSupportsErrorInfo,用来判断组件中某一接口是否支持截获异常。
STDMETHODIMP CParser::InterfaceSupportsErrorInfo(REFIID riid)
{
static const IID* arr[] =
{
&IID_IParser
};
for (int i=0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
if (InlineIsEqualGUID(*arr[i],riid))
return S_OK;
}
return S_FALSE;
}
...
也就是说:只有接口位于arr表中时,客户端才可以截获异常。
static const IID* arr[] =
{
&IID_IParser
};
总结:只有接口位于arr表中时,客户端才可以截获异常。但并不影响其在组件实现中抛出异常。
为什么要这么作呢?为了效率?