Assert-based Error Reporting in Delphi

[This is a very old article I wrote back in 2002 when I worked for a company which built MRI scanners and was subsequently bought by Oxford Instruments. The driver for this was “…Until Delphi acquires native functions equivalent to the C [__LINE__ and __FILE__] macros, … the need for this Assert-based framework … will remain” The need to trace errors to a specific class and line number, especially in production code, has only become stronger since then.]

Summary

This note describes a simple but flexible error-reporting and tracing framework for Delphi 4 and above based on Assert, which provides the unit name and line number at which errors were trapped and traces made.

Details

Background

Under the Delphi Language there is no simple way of replicating the C/C’++ macros FILE and _LINE_ to obtain the unit name and line number of memory address at runtime. However, in his paper “Non-Obvious Debugging Techniques” Brian Long points out that the Delphi compiler provides both unit name and line number during a call to Assert, and describes how assertions can be exploited to provide detailed execution tracing.

The framework described here extends this idea to allow flexibility in the processing of assertions. Assertion processing can be switched on and off at runtime; arbitrary filtering can be applied to any assertion; and both execution tracing and ‘standard’ assertion behaviour (i.e. raising an exception) can be effected. Assertions can therefore be left enabled in production code, at the expense of a slightly larger binary executable.

Design

If SysUtils is included in the uses clause of a unit within a project, the default Pascal-style Run-Time Error (RTE) error handling of the System unit is replaced with one based on exceptions. By default a failed assertion causes RTE 227, but with SysUtils an EAssertionFailed exception is raised instead, which normally just displays a message box:

procedure TForm1.Button1C1ick(Sender: TObject);
 begin
Assert(False, ‘Inside Button1Click’);
end;

Image2a

The default behaviour is changed by reassigning the AssertErrorProc variable to a custom routine. The failed assertion, along with all the unit and line number information, can then be extracted and passed to a logging or error recovery mechanism.

The library unit AssertLib.pas redirects assertion handling to its own custom routine; when an assertion fails, the message string is checked for special substrings, and the assertion logged or an exception thrown as appropriate. Assuming a hypotheical Assertion object, the general algorithm is:

// assertion handler routine
 it IsDebugString(Assertion.Messaqe) then
  LogDebugString
else if IsSevereError(Assertion.Message) then
   raise ESevereError...
 else
   raise ENormalError...;

The exception handling of the application or DLL is relied upon to deal with the exceptions raised. Crucial here is that the unit name and line number are extracted before the exceptions are raised, and stored in custom fields of the exception objects:

type EAssertDetails = class(Exception)
public
constructor CreatedDetailed( const Msg, AUnitName : string;
AlineNumber, AAddress : Integer);
   property UnitName : string read FUnitName write SetUnitName;
   property LineNumber : Integer read FLineNumber write SetLineNumber;
   property Address    Integer read FAddress write SetAddress; end;
EAssertDetailsFatal   = class(EAssertDetails);
 .
 .

 // inside the assertion handler
 else if IsSevereError(Assertion.Message) then
raise EAssertDetailsFatal.CreateDetailed( Assertion.Message, UnitName, LineNumber, ErrorAddress);

If the logging/exception framework is aware of these special fields, it can extract the information easily, and tailor its behaviour to the way in which Assert is being used:

procedure TForml.HandleException(Sender: TObject; E:Exception);
 var temp : string;
begin
if E is EAssertDetails then
begin
  // Extract unit name and line number from special exception object

 temp := Format (Sender %s reports exception: %s. + Unit: %s, Line: %d, Address: %d.’, [Sender.ClassName, E.Message, E.UnitName, E.LineNumber, E.Address]);
// Notify the user with a message dialog if required
   if E is EAssertDetailsNotify then ShowMessage(temp);
// The assertion was deemed fatal, so terminate the app
 if E is EAssertDetailsFatal then
ShowMessage(temp);
Application.Terminate;
end;

 // Add other specialized descendents of EAssertDetails as required

 end
else
temp := E.Message;

 // Log all errors
 Memol.Lines.Add(temp);
 end;

Image4a

Usage

Assert can be used to pinpoint error locations whenever it can be assumed that assertions are turned on ({C+}). For example, low-level code and support classes should not use assertions for error reporting, because they cannot rely on assertions being enabled. In general, if a section of code expects flow to be broken in an error condition, then exceptions must be used as normal.

User interface code, on the other hand, can usually use Assert in safety, because the exceptions raised in the custom assertion handler will be handled by code at the same level (probably with Application.OnException). Also, User Interface code can trap exceptions raised at a lower level, and then use Assert combined with Delphi’s Run-Time Type Information to report the exception:

procedure TrapException;
var i : Integer;
begin try
i := 10 div 0;
except
on E: Exception do
Assert(False, E.ClassName + ' ' + E.Message);
end;
end;

In this way, most of the information provided by the exception is retained (e.g. the class reference is lost), but we gain information about which section of code was executing when the exception was trapped.

Advanced Features

Because failed assertions are routed through a custom routine, their behaviour can be controlled at runtime. The GAssertEnabled flag in AssertLib determines whether a failed assertion should be acted upon or ignored. This behaviour is dependent  upon the instantaneous value of GAssertEnabled, so can be switched on and off for different sections of code.

As alluded to above, Assert can be used for debugging traces. By prepending the constant SDEBUGTRACE to the assertion error message, and forcing an assertion failure, line-by-line traces of a code path can be logged.

The custom AssertErrorProc checks for S_DEBUG_TRACE and passes the assertion information (including line number etc.) to a special debug trace procedure, set using SetDebugLogProcedure. In this case, an exception is not raised, so that the logic flow is not broken in the traced routine. Acknowledgement of these trace assertions can be controlled independently of other assertions, because it is assumed that trace assertions may be much more common than ‘error’ assertions. The flag GRecogniseDebugAssertions controls this behaviour:

procedure TraceMeBaby;
begin
  Assert(False, S DEBUG TRACE + ‘entering TraceMeBaby...’);
   // do some work
   // ...
   Assert(False, S_DEBUG_TRACE + ‘...leaving TraceMeBaby’);
 end;

Image3a

By defining other special prefixes and prepending them to assertion messages, the custom AssertErrorProc can distinguish further uses of Assert.

SSEVEREERROR is one such indicator; it is used to report severe but foreseen errors in program operation.

Limitations and Improvements

Because Assert is the only easy way to access unit name and line number information in Delphi, such information is only valid for the place where Assert was called (contrast this with the C/C++ macros __FILE__ and __LINE__ which can be used anywhere). The location of the Assert call may not correspond to the true location of the error, but rather to the point at which the error was detected. Until Delphi acquires native functions equivalent to the C macros, this limitation (and the need for this Assert-based framework) will remain.

Control of assertions could be made more extensible by being given a class wrapper, with the concept of registering different assertion conditions:

// Assertion class to call MessageBeep() on assertion failure
 EAssertDetailsBeep = class(EAssertDetails);
// Register new assertion class, using prefix identifier string and class reference
 AssertionManager.RegisterAssertionClass(‘[B]’, EAssertDetailsBeep);

‘This would allow arbitrary classes deriving from EAssertDetails to be defined outside AssertLib.pas; the AssertionManager would search a list of identifier prefixes for all registered assertion classes and raise the appropriate exception.

Another improvement would be to allow per-thread control of assertion behaviour, by marking switches such as GAssertEnabled with the threadvar keyword. This would solve some concurrency issues that would otherwise occur if the framework were used in a multithreaded application.

Conclusion

The ability to trace detection of an error to a specific line in the source code is very useful, but fully-featured frameworks to achieve this are by necessity complex. The framework described here requires no changes to existing code, other than AssertLib to be initialized after SysUtils, a small number of variables to be set, and assertions enabled.

Join the discussion...

This site uses Akismet to reduce spam. Learn how your comment data is processed.