HRESULTs indicate the success or failure of method calls in COM. They are particularly important in calls that extend across the network. COM has standardized on this return value so that the remoting infrastructure can inform the client that a problem has occurred when an object can't be reached. Therefore, if an object that is part of an active connection meets an untimely death, the client can discover this fact and respond in the appropriate manner.
All remotable methods return HRESULTs at the physical level to accommodate this requirement, but the Visual Basic compiler and the run-time layer can hide all traces of their existence. However, if you use OleView.exe and inspect your servers, you'll see that every method you have created is defined with an HRESULT as a return value—including Visual Basic methods that are defined as functions. For example, a method defined like this:
Function GetTaxRate() As Double
results in IDL code that looks like this:
HRESULT GetTaxRate([out, retval] double* );
Whenever a method needs a logical return value, IDL accommodates this by providing the [retval] parameter attribute. There can be only one [retval] parameter in a COM method, and it must appear last in the parameter list. Visual Basic and Java are examples of languages that transparently map these special parameters so that programmers can use them as return values instead of as output parameters. This makes calling a function more natural, and it also relieves programmers from manually inspecting HRESULTs. The HRESULT is inspected by Visual Basic's run-time layer, and the client receives an error in the event of a failure.
Sometimes the connection between a client and an object is broken in a distributed application. This can happen when the server crashes or when the server's computer is rebooted by an overzealous administrator. In either case, the client application will have an outstanding reference to an object that no longer exists. It can also happen when network problems make it impossible to route a method request between the client and the object. In all of these cases, a remote method call will fail. The underlying remote procedure call (RPC) layer is responsible for informing the client that the server is unavailable. The HRESULT was created for this purpose.
Visual Basic 6 added support to the run-time layer for catching most of the HRESULTs that indicate that an out-of-process object is dead or unreachable. When the Visual Basic run-time library determines that the RPC layer has reported a dead object, it raises error number 462 along with the description, "The remote server machine doesn't exist or is unavailable." This run-time error assists you in most of the cases in which you need to handle a dead or unreachable object. However, the Visual Basic run-time library doesn't handle certain other situations as elegantly. In these situations, the Visual Basic run-time library raises an error and places the raw HRESULT in Err.Number. Moreover, if you're using Visual Basic 5, you must always deal with HRESULTs in order to determine that an object has died. In times like these, it's very helpful to know exactly what's inside an HRESULT.
An HRESULT is a 32-bit value that contains three distinct pieces of information. Figure 5-6 shows how the physical memory of an HRESULT is segmented to hold a severity level, a facility code, and an application-specific error code. The severity level stored in bit 31 indicates whether the method call was successful. If the bit is left off, the method succeeded. If the bit is turned on, the method did not succeed.
The set of bits in the middle of an HRESULT, known as the facility code, informs the client where the error originated. Standard COM error codes use the code FACILITY_NULL. An object, on the other hand, should use FACILITY_ITF when it wants to send an interface-specific error code to the client. The list in Figure 5-6 shows some of the other possible values for the facility code. A number of facility code values aren't shown in this list, but Visual Basic programmers rarely need to deal with them directly.
Figure 5-6. All methods in COM return HRESULTs at the physical level.
You should be aware of a few important facility codes that inform the client application of a problem in the underlying RPC transport. You can test for these facility codes in a standard Visual Basic error handler to determine whether your call was unable to reach the object. Look at the code below.
Enum FacilityCodes FACILITY_RPC = 1 FACILITY_WIN32 = 7 FACILITY_WINDOWS = 8 End Enum Private Dog As IDog Private Sub Form_Load() ' Create dog on another computer. Set Dog = New CBeagle End Sub ' Test for dead object on user command. Private Sub cmdMakeDogBark() On Error GoTo Err_cmdMakeDogBark Dog.Bark Exit Sub Err_cmdMakeDogBark: Const vbDeadObject = 462 ' First: Test for Visual Basic dead object error. If Err.Number = vbDeadObject Then Set Dog = Nothing MsgBox "Your dog has unfortunately died." ' Second: Test for a bad facility code. ElseIf BadFacilityCode(Err.Number) Then MsgBox "Your dog might or might not be dead." End If End Sub Function BadFacilityCode(ByVal HRESULT As Long) As Boolean Dim FacilityCode As Integer ' Extract facility code from HRESULT. FacilityCode = ((HRESULT And &HFFF0000) / 65536) If FacilityCode = FACILITY_RPC Or _ FacilityCode = FACILITY_WIN32 Or _ FacilityCode = FACILITY_WINDOWS Then ' These facility codes signify a failed method call. BadFacilityCode = True End If End Function
When a Visual Basic client tries to invoke a method on a dead or unreachable object, the RPC layer returns an HRESULT indicating failure. If the Visual Basic run-time library knows about the HRESULT, it raises error number 462. This error is easy to trap and handle. However, if the Visual Basic run-time library doesn't know about the HRESULT, it simply raises an error and places the raw HRESULT in Err.Number. When this happens, you can examine the facility code in an error handler as shown in the utility function BadFacilityCode.
By examining the facility code, you can attempt to determine the nature of the problem. Facility codes such as FACILITY_RPC, FACILITY_WIN32, and FACILITY_WINDOWS can mean a dead or unreachable object. Unfortunately, you can't be sure that receiving one of these facility codes really means the object has died. You might receive a facility code when the parameter state of a method call couldn't be marshaled to or from the object. This means that the next call to the object might be successful. When you find that an HRESULT has a bad facility code, you should look at the error code in the lower 16 bits of the HRESULT if you really want to know what has happened. You can find a list of such error codes in the header file WINERROR.H, which is part of the Win32 SDK. As you can imagine, working at this level can be very tedious for the average Visual Basic programmer.
When you find that an object has died, you can always attempt to activate another one. If you are successful, you can then reinitialize the object and reexecute the method. Of course, the problem that killed the first object might prevent you from creating a new one. You can add your own fail-over support by attempting to create another object on a different machine. However, you can do this only if more than one remote computer can serve up the same CLSID. Many factors determine what you do next, but at least your client application knows exactly what has occurred and can degrade gracefully.
An HRESULT can convey whether a method has succeeded. It can also contain an application-specific error code. But it can't convey a text-based error message or any information about where the error actually occurred. COM uses COM exceptions for passing elaborate error information between objects and clients. Support for COM exceptions is provided by a handful of functions and interfaces in the COM library.
Before examining the inner workings of COM exceptions, you should note that all these details are hidden from Visual Basic programmers. The Visual Basic team has taken Visual Basic's proprietary error handling model and cleanly mapped it on top of COM exceptions. As a result, Visual Basic programmers don't have to know a thing about COM exceptions. Another, less obvious, benefit is that Visual Basic programmers can throw COM exceptions from their servers and catch COM exceptions in their client applications. This is extremely important when Visual Basic components are used in systems with COM components that were written with other tools and languages. As long as everybody follows the rules outlined by COM's language-independent exception model, everything works out fine.
Here's how COM exceptions work. Any COM object that throws exceptions must advertise this fact by supporting a standard interface named ISupportErrorInfo. This interface allows a client to query the object on an interface-by-interface basis to see whether it supports exceptions. This allows the object to tell the client, "Yes, I support COM exceptions for the interface you are using."
When an object wants to throw an exception, it must create an error object by calling a function in the COM library named CreateErrorInfo. This function creates the error object and returns an ICreateErrorInfo reference. The object then uses this reference to populate the newly created error object with information such as a description and an error source. Next the object must call SetErrorInfo to associate the error object with the logical thread of the caller. Finally the object must return an HRESULT that tells the client that something has gone wrong.
When an exception-savvy client inspects an HRESULT and finds that a method call was not successful, it queries the object to see whether it supports the ISupportErrorInfo interface. If it does, the client calls a method through this interface to test for support on the IID that is being used. If the object indicates that it supports exceptions on the IID, the client can be sure that an error object exists with contextual information about the nature of the problem. The client can then call GetErrorInfo to retrieve an IErrorInfo interface reference to the error object. This interface lets the client retrieve all the error information sent by the object. As you can see, COM exceptions require a lot of work on both sides and quite a few round-trips.
If you have used Visual Basic's error handling model, you know that it's much easier to use than COM's exception handling model. It is fortunate that the two models work so well together. Visual Basic programmers continue to raise and trap errors in the same way that they have for years, and the details are transparently mapped by the run-time layer to conform to the language-independent model required by COM.
Every coclass in Visual Basic has built-in support for ISupportErrorInfo. When you explicitly raise an error from a Visual Basic server with Err.Raise syntax, the run-time layer automatically creates and populates a COM error object. It also makes the other necessary calls to properly throw a COM exception. All you really need to do is provide code as demonstrated in the following example:
Dim Number As Long Number = vbObjectError + 2 Dim Source As String Source = "DogServer.CBeagle.Bark()" Dim Description As String Description = "The cat's got the dog's tongue" Err.Raise Number, Source, Description
When you call Err.Raise, you must pass a user-defined error code. Visual Basic provides the intrinsic constant vbObjectError as the starting point for your user-defined error codes. The previous example also shows how to populate the Source and Description properties of an error object. The source should tell the client application where the problem occurred. This is most useful during debugging. The description should give the client an indication of what went wrong in a human-readable form. It's common practice to display the description to the user, so keep that in mind. If the error condition has a remedy, it's often valuable to make that known in the description as well.
All of the user-defined errors raised by your servers should have corresponding error codes. The error codes for a server should be published in the type library using enumerations. You can define a set of error codes in the following manner:
' Dog Server Error Codes Enum ErrorCodesEnum dsDogUnavailable = vbObjectError dsDogUnagreeable dsDogNotCapable dsDogNotFound dsUnexpectedError End Enum
Visual Basic's client-side mapping of COM exceptions is just as transparent as it is on the object side. When a method returns an HRESULT indicating failure, the mapping layer uses the ISupportErrorInfo interface to determine whether the object supports COM exceptions. If it does, the mapping layer retrieves the COM error object and uses it to populate a standard Visual Basic Error object. The mapping layer then raises an error to the caller. You can use a Select Case statement in a client application to create a handler, like this:
Sub MakeDogBark(Dog As IDog) On Error GoTo MakeDogBark_Err Dog.Bark ' Exit if successful. Exit Sub ' Enter handler on error. MakeDogBark_Err: Select Case Err.Number Case dsDogNotCapable: ' Perform necessary handling in client. MsgBox Err.Description, vbCritical, "Error: Dog Not Capable" Case Else ' Always provide a last-chance handler. MsgBox Err.Description, vbCritical, "Unexpected Error" End Select End Sub
When a Visual Basic client application experiences an error on a COM object that doesn't support COM exceptions, the mapping layer raises an error and places the raw HRESULT into Err.Number. You also get a raw HRESULT when a call experiences an error in COM's infrastructure, such as when you attempt a method call on a dead object. When you see an error code that is a large negative number, such as -2147023174, this indicates that you are dealing with a raw HRESULT.
When you distribute COM servers, you are expected to follow the rules defined by COM as well as the rules used by Visual Basic programmers. This is especially true if you work in environments in which some components are written with tools and languages other than Visual Basic. This section covers some of the rules that you should keep in mind.
All COM exceptions that leave your server should be explicitly raised by your code. This means that only errors explicitly generated with Err.Raise syntax should leave your server. You must prevent any unhandled errors from leaving your server. Most Visual Basic programmers know that an unhandled error in a client application will result in the application's termination. A Visual Basic server, on the other hand, deals with an unhandled error by passing it on to the client. For instance, if a method in your server experiences a division-by-0 error that isn't handled, the run-time layer simply forwards the error on to the client application.
It's considered bad style to let exceptions leave your server that haven't been explicitly raised. Any COM object that implements ISupportErrorInfo can pass on its own exceptions only. This means that you must catch any Visual Basic-generated error in your servers. If your server experiences an error that it can't deal with, you should send an application-specific "unexpected error" exception back to the client.
Always include a helpful text description when raising an exception. Try to create informational descriptions at the place where the error occurred. The procedure in which the error occurs usually has the most contextual information. It's easy to propagate this description back to the original caller from anywhere in the call chain. A description such as "Order for $2000.00 could not be accepted. The customer has a credit balance of only $1250.00" is far more valuable than "Order submission failure." The handler in the client application will often display this error message to the user. Remember that error codes are for programmers and error descriptions are for users.
Always use enumerations to define your error codes. This will allow other programmers to see your error codes through the type library. Visual Basic programmers using your server will be able to see these error codes with the Object Browser.
Always define your error codes using vbObjectError. The lower error codes are reserved by COM and have special meanings. If you use the vbObjectError constant, you can be sure that your codes will not conflict and be misinterpreted as system-defined error codes.
Try to supply documentation with your servers that describes errors to other programmers. You can convey only so much information with an enumerated value such as dsDogNotCapable. If possible, you should include a list of remedies and workarounds for each error condition.
Never display a message box in your server's error handlers. The decision to interact with the user should always be left to the client application. With an out-of-process server, an error handler that displays a message box will hang the server. You don't want this to happen. Although you can display a message box to the user from an in-process server, this is considered bad style. The client application programmer should ultimately control all interaction with the user.