错误与异常处理---组件
在组件程序中,如果遇到错误,一般有两个方式进行处理。
1. 简单返回HRESULT
对于比较简单的错误,直接返回表示错误原因的 HRESULT。
2. 抛出COM异常---调用Error(...)
既然 COM 是靠各种各样的接口来提供服务的,于是很自然地就会想到, 是否有一个接口能够提供更丰富的错误信息报告那?答案是: IErrorInfo(调用SetErrorInfo(0, pErrorInfo);)。
ATL 把SetErrorInfo包装成 CComCoClass::Error() 的6个重载函数了。
函数调用过程如下:
Error --> AtlReportError --> AtlSetErrorInfo --> SetErrorInfo(0, pErrorInfo);
template
<
class
T,
const
CLSID
*
pclsid
=
&
CLSID_NULL
>
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:
inline HRESULT WINAPI AtlReportError(
const
CLSID
&
clsid, LPCOLESTR lpszDesc,
const
IID
&
iid
=
GUID_NULL, HRESULT hRes
=
0
)
{
return
AtlSetErrorInfo(clsid, lpszDesc,
0
, NULL, iid, hRes, NULL);
}
--> AtlSetErrorInfo:
ATLINLINE ATLAPI AtlSetErrorInfo(
const
CLSID
&
clsid, LPCOLESTR lpszDesc, DWORD dwHelpID,
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 方式调用组件
HRESULT hr
=
spXXX
->
fun()
//
调用组件功能
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 文件去看。
如:
inline _variant_t ISet::GetValue ( _bstr_t Name ) {
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的定义如下:
class
_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)然后抛出,会很繁的。
就像这样,处理每个可能的错误,都要一大串代码 ---晕!
HRESULT hr
=
spXXX
->
fun()
//
调用组件功能
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接口
class
ATL_NO_VTABLE CSample :
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表中时,客户端才可以截获异常。但并不影响其在组件实现中抛出异常。
为什么要这么作呢?为了效率?