Using COM-Rich Error Information in Managed Code

Team-Fly    

 
.NET and COM Interoperability Handbook, The
By Alan Gordon
Table of Contents
Chapter Seven.  Advanced .NET to COM Interop

Using COM-Rich Error Information in Managed Code

COM objects use HRESULTs and error objects to return error information to their clients . .NET objects always use exceptions to return error information to their client. The mapping between these two is fairly straightforward. Before I go into that mapping, let's review COM HRESULTs and error objects because it is possibleparticularly if you are a VB programmerto use HRESULTs and error objects without understanding what they are and how they work.

The usual way to return an error in COM is to return a 32-bit error number that is typed as an HRESULT. In fact, all methods defined in a COM interface should return an HRESULT even if they succeed. HRESULTS are partitioned into four bit fields as shown in Figure 7-6: (1) The high-order bit is a severity code where 0 indicates success and 1 indicates an error, (2) the next 2 bits are reserved, (3) the next 13 bits make up a facility code, and (4) the final 16 bits make up a description code that uniquely identifies the error or warning if there is one.

Figure 7-6. A COM HRESULT.

graphics/07fig06.gif

Some example facility codes are FACILITY_DISPATCH, which is used for IDispatch errors; FACILITY_RPC, which is used for RPC errors; and FACILITY_ITF, which is used for errors that are returned from Interface methods. All user -defined HRESULTS should use FACILITY_ITF.

A method should return S_OK to indicate that it returned successfully. The COM runtime will fill the HRESULT return value if the method call fails for a system-level instead of an application-level problem. For instance, if a DCOM call fails because of a network problem, you will probably get a RPC_E_COMM_FAILURE. Similarly, if the security subsystem determines that a user is not authorized to use a server, it will fill the HRESULT with an E_ACCESSDENIED error. If an application-level error occurs in your code (that is, the client attempts to add a stock to be monitored that is already being monitored ), you can either return one of many precanned HRESULTS, or you can create your own. The precanned HRESULTS include (among many others) E_FAIL, which indicates that an unspecified error has occurred; E_E_INVALIDARG, which is used to indicate that an argument passed to the method is invalid; or E_UNEXPECTED, which is used to indicate that some unexpected, catastrophic error has occurred (as if any catastrophic error is expected). The definitions of these HRESULTS from winerror.h are as follows :

 #define E_FAIL _HRESULT_TYPEDEF_(0x80004005L) #define E_INVALIDARG _HRESULT_TYPEDEF_(0x80000003L) #define E_UNEXPECTED         _HRESULT_TYPEDEF_(0x8000FFFFL) 

Of course, these errors are so generic that they give the client little useful information with which to fix the problem, so most COM servers define custom HRESULT values that they can return to the client. The following code shows a definition of two custom HRESULTS in the StockMonitor class:

 const E_STOCKNOTFOUND=     MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,0x200+103); const E_STOCKALREADYMONITORED=     MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,0x200+104); 

You create customer HRESULTs using the MAKE_HRESULT macro, which is also declared in the winerror.h file. The GetPrice method on the IStockMonitor interface will return the E_STOCKNOTFOUND HRESULT if you try to get the price of a stock that does not exist. The AddNewStock method will return the E_STOCKALREADYMONITORED HRESULT if you attempt to add a stock to the StockMonitor that is already being monitored. You can return a custom or standard HRESULT from your methods as shown in schematic form here:

 STDMETHODIMP CMyObject::GetPrice(BSTR ticker,float *price) {       if (MethodSuccessful())         return S_OK;       else         return E_STOCKNOTFOUND; } 

In most cases, though, you will want to return more information than just an error code to your client. You probably will want to send along a textual description of the error as well as information about the source of the error. You might even want to send along the path to a help file and a context ID that identifies the location in the help file where the user can go to read more about the error. To facilitate all of this, COM supports error objects. A COM error object is a COM object that implements the IErrorInfo interface. The IErrorInfo interface contains the methods shown in Table 7-3.

Table 7-3. Methods in the IErrorInfo interface

Method Name

Description

GetDescription

Returns a textual description of the error.

GetSource

Returns the ProgID of the class that returned the error.

GetGUID

Returns the GUID of the interface that generated the error.

GetHelpFile

Returns the path of the help file that describes the error.

GetHelpContext

Returns the help context ID for the error, which identifies a location within the help file.

Notice that there are no Set methods on this interface to initialize the information in the error object. To create an error object and to populate it with information, you must call the CreateErrorInfo COM API function, which returns an ICreateErrorInfo interface pointer. The ICreateErrorInfo interface contains the Set equivalents for all of the methods described in Table 7-3. After you have used the CreateErrorInfo function and the Set methods to build the error object, you call the SetErrorInfo method to set the object as an error for the current logical thread. The COM runtime will take care of marshaling the error object appropriately if the client resides in a different process or on another machine. The following code shows how you create, populate, and then send a COM error object to a client:

 STDMETHODIMP CStockMonitor::AddNewStock(BSTR ticker, float       price, short propensityToRise) {       map<CComBSTR,float>::iterator iter;       ObjectLock lock(this);       iter=m_StockPriceList.find(ticker);       if (iter==m_StockPriceList.end())       {         m_StockPriceList[ticker]=price;         m_StockPropensityList[ticker]=propensityToRise;         m_StockTickerList.push_back(ticker);         Fire_MonitorInitiated(ticker,price);         return S_OK;       }       else       {           IErrorInfo *pErrorInfo;           ICreateErrorInfo *pCreateErrorInfo;         HRESULT hr=CreateErrorInfo(&pCreateErrorInfo);         hr=pCreateErrorInfo->SetGUID(IID_IStockMonitor);         hr=pCreateErrorInfo->SetDescription(           OLESTR("This stock exists already"));         hr=pCreateErrorInfo->QueryInterface(              IID_IErrorInfo,(void**)&pErrorInfo);  hr=SetErrorInfo(0,pErrorInfo);  pCreateErrorInfo->Release();         pErrorInfo->Release();         return E_STOCKALREADYMONITORED;       } } 

The following code shows how you would catch the error on a client:

 void CStockserverclientDlg::OnAddstockButton() {       HRESULT hRes;       BSTR bstrTicker;       UpdateData(TRUE);       IErrorInfo *pErrorInfo;       BSTR bstrErrorMsg;       if (m_serverOK)       {         bstrTicker=m_ticker.AllocSysString();         hRes=m_pIStockMonitor->AddNewStock             (bstrTicker,m_price,m_propensity);         if (FAILED(hRes))         {           hRes=GetErrorInfo(0,&pErrorInfo);           if (SUCCEEDED(hRes))           {             pErrorInfo->GetDescription                   (&bstrErrorMsg);             AfxMessageBox(CString(bstrErrorMsg));           }           else             AfxMessageBox("Everything Failed");         }         ::SysFreeString(bstrTicker);       }       else         AfxMessageBox("The server failed to start."); } 

Fortunately, it is much easier to use COM errors in practice than you have seen here. ATL includes an Error function that encapsulates all of the logic required to create, populate, and send an error object. Using this function, I could rewrite the function shown previously as follows:

 STDMETHODIMP CStockMonitor::AddNewStock(BSTR ticker,     float price,short propensityToRise) {     map<CComBSTR,float>::iterator iter;     ObjectLock lock(this);     iter=m_StockPriceList.find(ticker);     if (iter==m_StockPriceList.end())     {         m_StockPriceList[ticker]=price;         m_StockPropensityList[ticker]=propensityToRise;         m_StockTickerList.push_back(ticker);         Fire_MonitorInitiated(ticker,price);         return S_OK;     }     else  return Error(   _T("This stock exists already"),   IID_IStockMonitor,   E_STOCKALREADYMONITORED);  } 

COM error objects are much easier to use from VB 6. You can use the raise method in the global err object to populate and return an error object to a client. The parameters to the raise function allow you to specify the source, description, help file, and so forth for the error object. The following code shows schematically how you would raise errors from VB code:

 Public Property Let CustomDictionary(ByVal vData As String)  On Error GoTo HandleErr  'The logic for this routine goes here...     Exit Property     'If an error occurs you will jump to here  HandleErr:  'we specify the error number, Source, and description  Err.Raise errDictionaryFileProblem, _   "SpellChecker.CustomDictionary", _   "Dictionary not found or could not be read"  End Property 

In comparison to COM error handling, .NET exception handling is simple. With .NET, when you want to tell your client that an error has occurred, you simply create an instance of an exception object, populate the fields in the object with information that is relevant to the error, and throw the object to your client. An example is shown here.

 void TestExceptions(float arg1) {       if (arg1 >= 0)       {       //... perform some processing       }       else         throw new ArgumentOutOfRangeException             ("arg1",arg1,"Argument cannot be negative"); } 

In this case, I throw an instance of the ArgumentOutOfRangeException class. Exception classes are customized to allow you to easily pass all the information that a client may need about the type of error that the exception class represents. In this case, the constructor for the ArgumentOutOfRangeException class has three arguments. The first argument is a string that contains the name of the argument that is out of range, the second argument contains the value of the offending argument, and the third argument contains a textual description of the error.

You can write the following code to catch the exception:

 private void cmdTestException_Click(object sender,       System.EventArgs e) {       try       {         TestExceptions(float.Parse(textBox1.Text));       }       catch (ArgumentOutOfRangeException ex)       {         MessageBox.Show("Out of range error: " +               ex.Message);       }       catch (Exception ex)       {         MessageBox.Show("Generic error: " + ex.Message);       } } 

Notice that I can write multiple catch statements for a single try block. If I do this, I must start with the most derived class and then have my base exception classes at the end. The CLR will only execute the try block that is closest to the actual type of the exception object. For instance, in this case, if the server threw an exception object of type ArgumentOutOfRangeException, the client would execute the catch statement associated with the ArgumentOutOfRangeException class. If the client threw any other exception, the client would execute the catch statement associated with the base Exception class.

Note

The ArgumentOutOfRangeException class, like most exception classes, has a number of overrides , but all of them allow you to construct an exception object with the same basic information: an argument name, its value, and a textual description.


You can use one of the many exception classes that the .NET Framework base class library provides for you, or you can create our own custom exception classes that derive from System.Exception or from another class that derives from System.Exception . The main reason for creating your own class is if there is information that you want to return to the client that is not provided by one of the standard classes. Table 7-4 lists just a few of the exception classes that the .NET Framework base class library provides.

Table 7-4. Useful exception classes

Method Name

Description

ApplicationException

Thrown when a nonfatal application error occurs. Use this class as the base class for your custom, nonfatal exception classes.

ArgumentException

Thrown when an invalid argument is passed to a method.

ArgumentNullException

Thrown when a null argument is passed to a method and derives from ArgumentException.

ArgumentOutOfRangeException

Thrown when an argument to a method is not within the allowable range and derives from ArgumentException.

InvalidOperationException

Thrown when a method call is not valid for the current state of an object.

NotImplementedException

Thrown when a method that is called on an object has not been implemented.

COMException

Thrown when an unrecognized or user-defined HRESULT is returned from a COM method call.

Note

I presented an introduction to exception handling in the .NET Framework in Chapter 4. In that chapter, I showed an example of creating a custom exception class.


All exception classes inherit from System.Exception , so it defines the set of methods and properties that are universally supported by all exceptions. The members supported by the System.Exception class are shown in Table 7-5. I ignored the standard System.Object methods.

Table 7-5. Members of the System.Exception class

Member Name

Type of Member

Description

Message

Property

Gets or sets the textual description of the exception.

Source

Property

Gets or sets the name of the application that caused the exception.

StackTrace

Property

Gets a stack trace at the time the exception occurred.

TargetSite

Property

Gets the name of the method that caused the exception.

InnerException

Property

Gets the exception that caused the current exception.

HelpLink

Property

Gets or sets a link to a help file that describes the exception.

HResult

Property

Gets or sets the HRESULT associated with this exception and is a protected property.

After you have a basic understanding of how COM error objects work, understanding the mapping between COM error objects and .NET exceptions is fairly simple. When a COM object returns an error (that is, an HRESULT with an error severity code such as E_NOINTERFACE), the CLR will convert the HRESULT to a managed exception and throw it to the client. The mapping is seamlessly handled by the CLR. The exception object will contain the HRESULT, but notice from Table 7-5 that it is a protected property. In most cases, you will not need to manipulate HRESULTs directly in your managed code. You can tell what type of error occurred by examining the type of the exception class that you receive. You can then write type-specific Catch statements with potentially different logic for each exception type. For many errors, especially those that originate in unmanaged code, you may still want to examine the HRESULT associated with the error. That's why some exception classes like System.Runtime.InteropServices.SEHException, System.Data.OleDb.OleDbException, System.Messaging.MessageQueueException, System.ComponentModel. Win32Exception, System.Web.HttpException , and (most important for this discussion) System.Runtime.InteropServices.COMException expose the underlying HRESULT as a public property called ErrorCode.

Note

The classes that expose the public ErrorCode property all derive from an exception class called System.Runtime.InteropServices.ExternalException. The ErrorCode property is declared in this class.


Some of the predefined HRESULTS like E_NOINTERFACE, E_NOTIMPL, and E_ACCESSDENIED map directly to exception classes. Table 7-6 shows the mapping between the most common predefined HRESULTS and .NET exception classes. The right-hand column in the table shows the .NET exception class you will see on a managed code client when a COM object returns the HRESULT in the left column. Notice that many of these exceptions simply map to an instance of System.Runtime.InteropServices.COMException with a customized message. In each of those cases, I show the custom message in parentheses

Table 7-6. Mapping between HRESULTS and exception classes

HRESULT

.NET Exception Class

E_FAIL

System.Runtime.InteropServices.COMException (Unspecified Error)

E_NOINTERFACE

System.InvalidCastException

E_ACCESSDENIED

System.UnauthorizedAccessException

E_NOTIMPL

System.NotImplementedException

E_INVALIDARG

System.ArgumentException

E_POINTER

System.NullReferenceException

E_OUTOFMEMORY

System.OutOfMemoryException

DISP_E_EXCEPTION

System.Runtime.InteropServices.COMException (Exception occurred)

DISP_E_PARAMNOTOPTIONAL

System.Runtime.InteropServices.COMException (Parameter not optional)

DISP_E_BADPARAMCOUNT

System.Reflection.TargetParameterCountException

DISP_E_DIVBYZERO

System.DivideByZeroException

DISP_E_UNKNOWNINTERFACE

System.Runtime.InteropServices.COMException (Unknown interface)

DISP_E_MEMBERNOTFOUND

System.Runtime.InteropServices.COMException (Member not found)

If, instead of using one of the predefined HRESULTs, you create a user-defined HRESULT as shown here and return it to the client, the CLR will convert the HRESULT to an exception of type System.Runtime.InteropServices.COMException .

 const E_STOCKALREADYMONITORED=       MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,0x200+104); 

Regardless of whether you are using one of the predefined HRESULTs or a user-defined one, the exact contents of the exception object that the CLR will throw when the error originates in unmanaged code depends on whether the unmanaged code sets an error object or not. If the unmanaged code sets an error object by calling the Error function in ATL or by calling err.Raise in VB, the fields of the managed code exception object will be populated from the COM error object as shown in Table 7-7.

Table 7-7. The mapping between fields in a COM error object and .NET exception classes when you populate the fields of the COM error object

Where the Information Comes From in the COM Error Object

.NET Exception Field

IErrorInfo->GetDescription()

Message

IErrorInfo->GetSource()

Source

The top of the stack is the unmanaged code (COM) method that generated the error. The bottom of the stack is the managed code method that caught the error.

StackTrace

The name of the unmanaged code method that generated the exception

TargetSite

The HRESULT returned by the COM method

ErrorCode (available on COMException and other exception classes that derive from System.Runtime.InteropServices. ExternalException)

Null

InnerException

if (IErrorInfo->GetHelpContext() != 0)
    IErrorInfo->GetHelpFile() +"#" +
    IErrorInfo->GetHelpContext()
Else
    IErrorInfo->GetHelpFile()

HelpLink

If you return an HRESULT from your COM object, but do not populate the rich error information (COM error object) as follows, the .NET exception object will be populated by the CLR as shown in Table 7-8.

 const E_STOCKALREADYMONITORED=       MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,0x200+104); STDMETHODIMP CStockMonitor::AddNewStock(BSTR ticker,       float price, short propensityToRise) {       map<CComBSTR,float>::iterator iter;       ObjectLock lock(this);       iter=m_StockPriceList.find(ticker);       if (iter==m_StockPriceList.end())       {         m_StockPriceList[ticker]=price;         m_StockPropensityList[ticker]=propensityToRise;         m_StockTickerList.push_back(ticker);         Fire_MonitorInitiated(ticker,price);         return S_OK;       }       else         return E_STOCKALREADYMONITORED; } 

Notice that, in this C++ version of the AddNewStock function, I do not call the ATL Error function when I return with an error, which populates the error object. Instead I just return the HRESULT.

Table 7-8. Source of data for .NET exception classes when there is no COM error object

Where the Information Comes From

Exception Field

"Exception from HRESULT: [HRESULT in hex form]"

Message

The name of the Interop assembly that threw the exception, e.g., Interop.StockserverLib

Source

The top of the stack is the unmanaged code (COM) method that generated the error. The bottom of the stack is the managed code method that caught the error.

StackTrace

The name of the unmanaged code method that generated the exception

TargetSite

The HRESULT returned by the COM method

ErrorCode (available on COMException and other exception classes that derive from System.Runtime.InteropServices.External-Exception)

Null

InnerException

Null

HelpLink

The Message field in the Exception object will contain the string: "Exception from HRESULT: [HRESULT in hex form]" where [HRESULT in hex form] is the value of the HRESULT in hex e.g.,

 Exception from HRESULT:  0x80040268 

Notice that the StackTrace, TargetSite, and ErrorCode fields are populated exactly the same as they are populated when the unmanaged object sets an error object. The main difference is the contents of the HelpLink field, which can only be populated by a COM error object and therefore will be NULL if you do not create an error object.


Team-Fly    
Top
 


. Net and COM Interoperability Handbook
The .NET and COM Interoperability Handbook (Integrated .Net)
ISBN: 013046130X
EAN: 2147483647
Year: 2002
Pages: 119
Authors: Alan Gordon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net