An improved Abstract Error Handler for Delphi

NOTE: The original blog post was wrong in one point: The change happened in Delphi XE2, not 10.2. I have now updated this and also adapted the conditional compilation in u_dzAbstractHandler. I should also mention, that the code only works on Win32. Win64 and other platforms are not supported. But since that unit won’t compile for other platforms that is pretty obvious.
— 2024-06-05 twm

Back in the lte 1980s when Borland released an object-oriented version of Turbo Pascal they introduced the abstract directive. It is used to declare a virtual method of an object that is usually called by other methods of that object but must be implemented by descendants.

The syntax has changed a bit since the times of Turbo Pascal. Delphi now uses the class keyword instead of object and overrides a virtual method using the override directive rather than virtual, but the general principle is the same.

type
  TAbstractAncestor = class
   protected
     procedure AbstractMethod; virtual; abstract;
   public
     procedure SomeMethod;
  end;

  TImplementorOfAncestor = class
  protected
     procedure AbstractMethod; override;
  end;

procedure TAbstractAncestor.SomeMethod;
begin
  // do stuff
  AbstractMethod;
  // do more stuff
end;

procedure TImplementorOfAncestor.AbstractMethod;
begin
  // do other stuff
end;

If you instantiate TAbstractAncestor, or if you instantiate a descendant class that does not override AbstractMethod, modern Delphi compilers will issue a warning

[dcc32 Warning] u_AbstractHandlerTest.pas(21): W1020 Constructing instance of ‘TAbstractAncestor’ containing abstract method ‘TAbstractClass.AbstractMethod’

So usually you will find such errors very quickly. Ancient Delphi Versions (e.g. Delphi 3 in the late 1990s) did not emit that warning and even with the latest and greatest Delphi 12 (yes, sarcasm) you can write code that instantiates abstract classes without getting such a warning, e.g. when using the Factory pattern.

So, what happens, when such an abstract method ends up being called? You will get an EAbstractError exception.

EAbstractError exception dialog

Pressing Break will then leave you at some rather unhelpful code in the debugger.

Debugger information after EAbstractError

The code editor shows you the source code of the AbstractErrorHandler procedure in the System unit. But at least the call stack nowadays contains the position where your code called the abstract method, in this case the procedure Main in u_AbstractHandlerTest. If I remember correctly, in the ancient Delphi versions the call stack did not contain that information, so we had to guess where that call came from (my memory might be flawed here).

Allan, my former boss at fPrint UK Ltd., back then wrote a small piece of code that changed this annoying situation. With this code the debugger stopped where the abstract method was called:

Debugger showing the call of the abstract method

I have been using this unit ever since and put it into my dzlib library.

unit u_dzAbstractHandler;

interface

uses
  SysUtils,
  SysConst;

implementation

procedure XAbstractErrorHandler;
const
  StackOffset = 8;
var
  p: Pointer;
begin
  asm
    mov eax,[ebp+StackOffset]
    mov p,eax
  end;
  raise EAbstractError.CreateResFmt(PResStringRec(@SAbstractError), ['']) at p;
end;

initialization
  AbstractErrorProc := XAbstractErrorHandler;
end.

So, what does it do? The assembler code retrieves the return address of the caller from the stack and then raises the EAbstractError exception at that address rather than the current execution point. Pretty neat, isn’t it? Simply by adding this unit to a project, finding the calling point for an abstract method got so much simpler.

That code has served me well over many years and many Delphi versions. But recently Embarcadero changed something in the RTL that lead to an access violation rather than an EAbstractError exception. I only recently found out about that change. After a bit of poking around in the CPU Debug window, it turned out that the StackOffset there is no longer 8 but 12. So the fix was easy: Check for the compiler version and use the correct StackOffset. But since I use multiple Delphi version in parallel I had to find out which version introduced that change. So I started with Delphi 6 and going forward found that the change came with Delphi XE2 and still is active in Delphi 12. So here is the fixed code:

unit u_dzAbstractHandler;

{$INCLUDE 'dzlib.inc'}

interface

uses
  SysUtils,
  SysConst;

implementation

procedure XAbstractErrorHandler;
const
{$IFDEF DELPHIXE2_UP}
  StackOffset = 12;
{$ELSE}
  StackOffset = 8;
{$ENDIF}
var
  p: Pointer;
begin
  asm
    mov eax,[ebp+StackOffset]
    mov p,eax
  end;
  raise EAbstractError.CreateResFmt(PResStringRec(@SAbstractError), ['']) at p;
end;

initialization
  AbstractErrorProc := XAbstractErrorHandler;
end.

That fixed unit of course is now also in the dzlib repository on SourceForge.