在组件程序中,如果遇到错误,一般有两个方式进行处理。
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表中时,客户端才可以截获异常。但并不影响其在组件实现中抛出异常。