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 possible—particularly if you are a VB programmer—to 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.


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 NameDescription
GetDescriptionReturns a textual description of the error.
GetSourceReturns the ProgID of the class that returned the error.
GetGUIDReturns the GUID of the interface that generated the error.
GetHelpFileReturns the path of the help file that describes the error.
GetHelpContextReturns 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 NameDescription
ApplicationExceptionThrown when a nonfatal application error occurs. Use this class as the base class for your custom, nonfatal exception classes.
ArgumentExceptionThrown when an invalid argument is passed to a method.
ArgumentNullExceptionThrown when a null argument is passed to a method and derives from ArgumentException.
ArgumentOutOfRangeExceptionThrown when an argument to a method is not within the allowable range and derives from ArgumentException.
InvalidOperationExceptionThrown when a method call is not valid for the current state of an object.
NotImplementedExceptionThrown when a method that is called on an object has not been implemented.
COMExceptionThrown 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 NameType of MemberDescription
MessagePropertyGets or sets the textual description of the exception.
SourcePropertyGets or sets the name of the application that caused the exception.
StackTracePropertyGets a stack trace at the time the exception occurred.
TargetSitePropertyGets the name of the method that caused the exception.
InnerExceptionPropertyGets the exception that caused the current exception.
HelpLinkPropertyGets or sets a link to a help file that describes the exception.
HResultPropertyGets 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_FAILSystem.Runtime.InteropServices.COMException (Unspecified Error)
E_NOINTERFACESystem.InvalidCastException
E_ACCESSDENIEDSystem.UnauthorizedAccessException
E_NOTIMPLSystem.NotImplementedException
E_INVALIDARGSystem.ArgumentException
E_POINTERSystem.NullReferenceException
E_OUTOFMEMORYSystem.OutOfMemoryException
DISP_E_EXCEPTIONSystem.Runtime.InteropServices.COMException (Exception occurred)
DISP_E_PARAMNOTOPTIONALSystem.Runtime.InteropServices.COMException (Parameter not optional)
DISP_E_BADPARAMCOUNTSystem.Reflection.TargetParameterCountException
DISP_E_DIVBYZEROSystem.DivideByZeroException
DISP_E_UNKNOWNINTERFACESystem.Runtime.InteropServices.COMException (Unknown interface)
DISP_E_MEMBERNOTFOUNDSystem.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 exceptionTargetSite
The HRESULT returned by the COM methodErrorCode (available on COMException and other exception classes that derive from System.Runtime.InteropServices. ExternalException)
NullInnerException
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 FromException Field
“Exception from HRESULT: [HRESULT in hex form]”Message
The name of the Interop assembly that threw the exception, e.g., Interop.StockserverLibSource
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 exceptionTargetSite
The HRESULT returned by the COM methodErrorCode (available on COMException and other exception classes that derive from System.Runtime.InteropServices.External-Exception)
NullInnerException
NullHelpLink

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.33.107