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.
Pressing Break will then leave you at some rather unhelpful code in the debugger.
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:
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.