Run-time call-stack tracing with Delphi
We are used to inspect a call-stack when something goes wrong in our application and it crashes. Delphi has excellent add-on tools that log detailed call-stack information when an application crashes.
But it is not always when an application crashes that call-stack information can be useful. We experience ourselves from time to time that having detailed call-stack information during a regular running application could help us trace undesired situations. Recently, it happened that in rare cases, a component property ended up with an unexpected value. In the application where the component was used, it was from many parts of the code and VCL framework itself that this property was set and it was mostly a matter of knowing from where exactly the unexpected value was set. Once we knew, we could understand and address the problem. The ability to just take at runtime a snapshot of the call-stack when a specific value was set was sufficient to find the culprit.
While TMS MemInsight will also give you at run-time insight into the call-stack when exceptions occur, now there is a simple method GetCallStack(sl: TStrings) that you can call from anywhere and as many times as you want and it will return detailed call-stack information. Only requirement is that you link your application with a detailed map information (see Linker options under Project options)
How it works?
Imagine there is a class TMyClass with a property Text:
TMyClass = class(TPersistent) protected procedure SetText(const Value: string); public property Text: string read FText write SetText; end;
Now, we want to know from where exactly in our application, this class Text property would be set with an empty string value.
We can write the property setter in the following way:
uses TMS.MI.Core; interface type TMyClass = class(TPersistent) protected procedure SetText(const Value: string); public property Text: string read FText write SetText; end; implementation proceduer TMyClass.SetText(const Value: string); var sl: TStringList; begin FText := Value; if FText = '' then begin sl := TStringList.Create; try GetCallStack(sl); sl.SaveToFile('c:tempcallstack.log'); finally sl.Free; end; end; end;
Now, at application level we can use this class for example like:
MyClass.Text := Edit1.Text;
and when the text property is being set with an empty string, we’ll get a nice stack-trace report giving us the information we wanted.
For this example, we create at TForm class level the method to set the myClass.Text via:
TForm1 = class(TForm) private { Private declarations } procedure SetClassText(s: string); end; implementation procedure TForm1.SetClassText(s: string); begin myclass.Text := s; end;
and when we call this from both the an edit control’s OnChange event and a form button OnClick event:
procedure TForm1.Button1Click(Sender: TObject); begin SetClassText(Edit1.Text); end; procedure TForm1.Edit1Change(Sender: TObject); begin SetClassText(Edit1.Text); end;
Now, we get a nice stack trace log when the class Text property is set with an empty string value:
TMyClass.SetText (57)
TForm1.SetClassText (77)
TForm1.Button1Click (43)
TControl.Click (7443)
TWinControl.WndProc (10162)
TButtonControl.WndProc (2617)
....
indicating this was called from the button OnClick.