Error handling
In this chapter the handling of errors in automation servers and
in automation clients is discussed.
An exception occurs
In every piece of software things (can) go wrong. This can be due to anything
from bad coding to hardware failure. Automation servers are no exception to
this. Whenever an error occurs in a piece of Delphi code an exception is raised.
Exceptions are the windows standard of dealing with errors, they provide a place
for at least an error-message and an error-code.
Let's take a very simple server DoesBang, with a buggy method Boom
procedure TDoesBang.Boom;
begin
ShowMessage(Format('%s', [123456]));
end;
This server will be used by a VBA client. VBA is very well capable of
catching exceptions using On error. The goto statements are a horror to a
true Delphi developer, but the code does catch the exception and show its
information.
Dim Bang As New DoesBang
Sub DoBang()
On Error GoTo CatchError
Bang.Boom
MsgBox ("Made it")
NormalExit:
Exit Sub
CatchError:
MsgBox (Err.Description & " The hex errorcode is : " & Hex(Err.Number))
GoTo NormalExit
End Sub
When the dobang macro is run Word pops up a dialog:

This message looks very familiar. In the method I passed a numeric value to the format
procedure and the string to be formatted needed a string value. This raises an
exception and the message is format's complaint. The exception raised in the Delphi code seems
to be
caught and understood by VBA.
At a first glance its seems if an exception is raised in the automation
server and is handled in Word. But according to the COM specifications an automation
server has to handle all exceptions itself, not a single one should leak through. To
understand what is going on we will have to take a deeper look at the code in
the server.
The buggy method is declared as a safecall:
procedure Boom; safecall;
Delphi Exceptions occurring inside a safecall are handled different. Control
will be passed to tObject.SafeCallexception. The implementation can be found in
the VCL unit system.pas
function TObject.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult;
begin
Result := HResult($8000FFFF); { E_UNEXPECTED }
end;
This result matches the error code displayed by VBA. It is cast to an hResult,
this hResult can be found in the declaration of Boom in the
typelibrary
HRESULT _stdcall Boom( void );
Every method of an automation server is actually a function with an hResult result type.
In the first chapter I showed how the
"functional" function result is a part of the parameter list. If
all goes well in the method an hResult of S_OK ($0) is returned by the
function. And in
the case of an exception E_UNEXPECTED ($8000FFFF) is returned thanks to tObject.SafeCallexception.
An hResult is a handle to a result. It is a 32 bit value in which the first
bit indicates success (a value of 0) or failure ( a value of 1). $8000FFFF forms
these 32 bits : 10000000000000001111111111111111. The first bit is a 1 so
VBA knows the call to boom was a failure and will call VBA's error handling
system. Follow 15 zero bits, this is FACILITY_NULL. Closing is the error number
FFFF, the final error holding the nice message "catastrophic failure".
Returning hResult error codes
Delphi translates any exceptions in my server's code to an automation
compatible format. The useful information the client received was a string
accompanied by a very general hResult, E_UNEXPECTED.
Some error conditions are very well recognized by the server. In which case
the server would like to raise an exception to pass this info to the client. I
can use an hResult again. The Delphi VCL has an automation exception type ready,
EoleSysError. This exception class has a constructor which takes an hResult.
An hResult has a length of 32 bits and consist of three parts. The first bit
indicates the success, being SEVERITY_ERROR or SEVERITY_SUCCESS, both are
declared in windows.pas. The next 15 bits are the facility again, they indicate
the group the error belongs to. All errors in custom automation servers should
use FACILITY_ITF, also declared in windows.pas. The third part is the
error-number itself, which should have a value between $0200 and $FFFF. When
raising
an error I will construct my own hResult
const MyError1 = 1;
.....
raise EoleSysError.Create('My personal error',
(SEVERITY_ERROR shl 31) or (FACILITY_ITF shl 16) or ($0200 + MyError),
0);
The last parameter is the helpcontext. The helpfile is a property of the
automation server and can be defined in the typelibrary or can be set at runtime
by setting the helpfilename property of the global ComServer
object.
When VBA executes a method in my server, and the server hits this error the
messagebox pops up again :

VBA understands the exception quite well and will displays it's
info.
Catching errors in the automation server
We have seen how VBA catches the exceptions and works with the hResult. An
automation client written in Delphi uses the same technique. When a call to
method leads to a SEVERITY_ERROR hresult Delphi will translate this in raising
an exception of type EoleException.
This piece of code from a Delphi project named bugclient calls the buggy method:
fBang:= CoDoesBang.Create;
fBang.Boom;
It will result in a well known Delphi exception dialog :

My server returned an hresult which is is marked as SEVERITY _ERROR. As a
reaction to this value The COM api on the client will raise an EoleException.
This exception can be caught in a try except block. This can be
used to retrieve the specific hResult
try
fBang.Boom;
except
on E : EoleException do
ShowMessage(Format ('%s The Hex errorCode is %x',[E.Message, E.ErrorCode ]));
end;
The exception is caught and the hResult I constructed in the server can be
found in the errorcode property of EoleException

Now I am catching all OLE Exceptions in the client. The setting of an hResult In
tObject.SafeCallException has the same result as explicitly raising an
EoleException in a server method. All end here. The difference between these
exceptions as the client sees them is the Factility value, a catastrophic
failure has FACILITY_NULL and in my EoleSysException I was given FACILITY_ITF.
If I am lucky the server has provided some info in the Errorcode or Message, if I am in bad
luck there is only $800FFFF there.
Catching errors when programming the COM API
hResult's are everywhere in the automation API. All helper functions return an
hResult. Whatever terrible things happen in there, you will miss it if you don't check
the function's result. To avoid having to code 10 level deep if statements the
VCL came up with the OLEcheck helper function.
procedure OleCheck(Result: HResult);
begin
if not Succeeded(Result) then OleError(Result);
end;
The result of the API function is passed in the param. It is checked for the
error bit. If this is set the OleError helper function will be called, passing the
hResult.
procedure OleError(ErrorCode: HResult);
begin
raise EOleSysError.Create('', ErrorCode, 0);
end;
All this helper does is raise an OLE exception, passing its constructor the
hResult. Using OleCheck you can program like this :
try
OleCheck(CreateBindCtx(0, BindCtx));
OleCheck(GetRunningObjectTable(0, ROT));
OleCheck(CreateClassMoniker(CLASS_SharedZapper, IMonik));
OleCheck(ROT.Register(0, Imonik, Imonik, cookie));
except
raise;
end;
I am trying to make 4 calls on the COM API. What these calls do is part of a
later chapter, for the moment it is enough to know that each of them can go
wrong. This will be noticed by the OLEcheck helper which will raise an
exception. By re-raising the exception in the except/end block I will be
confronted with the error message of the failing API call.
Where are we ?
Success or failure of an automation call is described in the hResult code.
When an exception occurs there is a lot going on behind the
scenes. The Delphi runtime system takes very good care of exceptions
leaving and exceptions entering your code, making the handling of exceptions
across COM borders fully transparent. Which means you do not have to take explicit care
of exceptions.
To pass specific error information to
clients a server should raise an EoleSysError with a custom hResult. Incoming
exceptions are of type EoleException and can be inspected as such to obtain the
hResult.
What's next ?
|