One feature I often miss when using Delphi is the support for proper exception stack traces at run-time. You know, those useful stack traces that show you exactly where an exception occurred, ideally with the method name and line number of where the exception was raised. Both .NET and Java have excellent stack trace support built right into the framework and the Exception classes. You just call Exception.StackTrace (.NET) or Exception.getStackTrace (Java) and get a detailed analysis of where the exception was thrown and how it got passed to your exception handler.
Unfortunately, Delphi never had good (built-in) run-time support for stack traces. The features for stack traces during debugging in the IDE are/were okay, but there were nothing in the language or framework which helped you to find out programmatically where an exception occurred at run-time and, more importantly, how it got passed to your exception handler (besides the original exception address, maybe). So, I was happy to see that Delphi 2009 finally introduced a new StackTrace property which, I hoped, would return a full-blown stack trace when you caught an exception.
The initial happiness soon wore off when I realized that the StackTrace property was really just a placeholder to return a stack trace from a possible stack trace provider rather than a real stack trace implementation. So, without such a provider (and there’s none that comes directly with Delphi), there is still no way to get a stack trace for your exceptions. Though a bit disappointing, the good thing is that there’s now finally a standardized way to get a stack trace, even if it’s not implemented by default.
Exception reporting tools such as Eurekalog or madExcept or debug helpers such as the JclDebug unit can register themselves as providers and use their engines to return a stack trace when an exception is raised. I’ve built a small unit which demonstrates this with the Jcl in combination with our logging tool SmartInspect and I’ve heard Fabio of Eurekalog is working on a similar feature for his component:
[sourcecode language='delphi']
unit StackTrace;
interface
uses
SysUtils, Classes, JclDebug;
implementation
function GetExceptionStackInfoProc(P: PExceptionRecord): Pointer;
var
LLines: TStringList;
LText: String;
LResult: PChar;
begin
LLines := TStringList.Create;
try
JclLastExceptStackListToStrings(LLines, True, True, True, True);
LText := LLines.Text;
LResult := StrAlloc(Length(LText));
StrCopy(LResult, PChar(LText));
Result := LResult;
finally
LLines.Free;
end;
end;
function GetStackInfoStringProc(Info: Pointer): string;
begin
Result := string(PChar(Info));
end;
procedure CleanUpStackInfoProc(Info: Pointer);
begin
StrDispose(PChar(Info));
end;
initialization
// Start the Jcl exception tracking and register our Exception
// stack trace provider.
if JclStartExceptionTracking then
begin
Exception.GetExceptionStackInfoProc := GetExceptionStackInfoProc;
Exception.GetStackInfoStringProc := GetStackInfoStringProc;
Exception.CleanUpStackInfoProc := CleanUpStackInfoProc;
end;
finalization
// Stop Jcl exception tracking and unregister our provider.
if JclExceptionTrackingActive then
begin
Exception.GetExceptionStackInfoProc := nil;
Exception.GetStackInfoStringProc := nil;
Exception.CleanUpStackInfoProc := nil;
JclStopExceptionTracking;
end;
end.
[/sourcecode]
The unit merely starts and stops the exception tracking of the Jcl, implements a minimal stack trace provider and registers for the Exception provider events. The GetExceptionStackInfoProc and CleanUpStackInfoProc functions are automatically called by the RTL to give the provider the opportunity to initialize and cleanup the stack trace after an exception occurred. GetStackInfoStringProc is called indirectly when you access the StackTrace property and is responsible for returning the actual stack trace.
So, how do you use this unit? Let’s have a look at the following example:
[sourcecode language='delphi']
…
uses
…, StackTrace;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
procedure SomeMethod;
end;
implementation
procedure TForm1.Button1Click(Sender: TObject);
begin
try
SomeMethod;
except
// Log the exception: We use SmartInspect here because it has
// built-in support for Exception.StackTrace but you could also
// access the StackTrace property here directly.
SiMain.LogException;
end;
end;
procedure TForm1.SomeMethod;
begin
raise Exception.Create(‘A test exception’);
end;
…
[/sourcecode]
As you can see, using this unit is just a matter of adding it to our uses clause. It won’t get any simpler than that. The unit will take care of registering/unregistering itself as a stack trace provider and when you now access the StackTrace property of an Exception object, you will get a detailed stack trace. To include the method names and line numbers in the stack trace, make sure to let the linker include debug symbols into your application and to enable the ‘Use debug .dcus’ option in case you also want line numbers from the VCL and RTL methods.
The SmartInspect Console showing the stack trace of an exception
Now, when you use SmartInspect for logging and have a stack trace provider registered, all your logged exceptions automatically include the exception’s call stack. Pretty useful, isn’t it?