EurekaLog
Menu

The EurekaLog Developer Guide

11. ECallStack and EDebugInfo: addresses, locations and symbols

If a report shows addresses but no useful names, distinguish two jobs before changing settings: tracing finds return addresses on a thread's stack; symbol lookup maps each address to a module, routine and source line. ECallStack does the first job and EDebugInfo coordinates the second. A successful trace does not prove that matching debug information is available.

For one location, use CurrentAddress or ReturnAddress with a lookup (section 11.2). For the current thread's whole stack, use GetCurrentCallStack (section 11.4). Capture another thread only when needed: suspension has real deadlock risks (section 11.5). Use DebugDetail to judge how much of a resolved location is actually known, rather than assuming a line number will always exist.

The examples require a correctly processed EurekaLog build and matching symbols for useful source names. Chapters 2 and 4 cover preserving and locating those symbols; this chapter covers the runtime API.

11.1 How a call stack is represented

A stack object is a list you can traverse through Count and Items. Each item carries a location and a thread ID. Tracing populates addresses; optional debug lookup fills the location's names and line number. Turn lookup off when you need raw addresses only.

The unit publishes four classes, one of which is the base, plus two type aliases and a metaclass:

TEurekaBaseStackList = class(TCheckedInterfacedPersistent);   // base, abstract
TEurekaStackListV6 = class(TEurekaBaseStackList);             // EurekaLog 6 method
TEurekaStackListV7 = class(TEurekaBaseStackList);             // EurekaLog 7 RAW method
TGenericStackList = class(TEurekaBaseStackList);              // arbitrary tracing methods
TEurekaCallStack = TEurekaStackList;                          // ECallStack
TEurekaStackList = TEurekaStackListV7;                        // ECallStack
TEurekaCallStackClass = class of TEurekaBaseStackList;        // ECallStack

TEurekaBaseStackList is described as "Base call stack class" and as a base abstract class whose instances you must not create; TGenericStackList, the fourth class, is the common stack tracing class for arbitrary tracing methods (section 11.3).

Both aliases point at the V7 class: they are "generic name, which will be changed in next EurekaLog versions. Use this name to refer the latest possible method." The class reference type is what lets a variable hold "whichever stack class is current" - both the global EurekaCallStackClass and the result of GetTracerClass have it.

The three stack pointers

These are the raw CPU stack positions, and they are the arguments the constructor later asks for. The CPU stack grows backwards, so their descriptions read inverted, and the documentation says so.

public
class function GetCurrentStackPointer: Pointer; static;            // ECallStack - TEurekaBaseStackList
class function GetCurrentTopOfStack: Pointer; static;              // ECallStack - TEurekaBaseStackList
class function GetCurrentStackFrame: ETypes.PStackFrame; static;   // ECallStack - TEurekaBaseStackList

GetCurrentStackPointer returns the most recent position of the stack: the logical end, physical beginning. GetCurrentTopOfStack returns the greatest pointer possible: the logical beginning, physical end. GetCurrentStackFrame is "very similar to GetCurrentStackPointer, except it is adjusted to point to stack's frame block" - with two notes: on x86-64 use GetCurrentStackPointer instead, and, in bold, do not use this function to walk through the stack; use Caller or GetTracer.

The fourth position helper supplies the address the stack is missing:

public
class function GetCurrentInstruction: Pointer; static;   // ECallStack - TEurekaBaseStackList

GetCurrentInstruction supplies the current execution point in the code that called it. An ordinary call stack contains callers, not that current instruction; pass this pointer as AFirstAddr when you want it at the start of the list. Section 11.3 demonstrates that call.

11.2 The addresses you start from

function CurrentAddress: Pointer;   // ECallStack
function ReturnAddress: Pointer;    // ECallStack

CurrentAddress is a pointer to the currently executing CPU opcode; ReturnAddress is a pointer to the CPU opcode following the instruction that called the current function - an address inside the caller. Both are documented for the same two uses: to build a call stack, or to obtain a human-readable description of a location. These complete helper functions belong in a unit using ECallStack and EDebugInfo:

function DescribeHere: string;
begin
  Result := GetLocationInfoStr(CurrentAddress);
end;

function DescribeCaller: string;
begin
  Result := GetLocationInfoStr(ReturnAddress);
end;

DescribeHere resolves an instruction inside itself; DescribeCaller resolves the return location in its caller. Call them from an ordinary application procedure when investigating the distinction, or put the lookup directly at the location you want to record. The exact routine, line and offsets depend on the build and available symbols; a source line such as "42" is not a promised output. Neither helper captures the location of an earlier exception—use its recorded information for that.

TEurekaStackList.Caller

TEurekaStackList.Caller is another documented way to obtain the caller. This VCL example belongs in an existing TForm1 with Label1; invoke it through the form's ordinary main-thread button event, not from a EurekaLog callback. Add these units to the form unit and enable the compiler's Stack Frames option:

uses
  ECallStack, // for TEurekaStackList
  EBaseClasses, // for TELLocationInfo
  EDebugInfo; // for GetLocationInfo

procedure TForm1.Button1Click(Sender: TObject);
  procedure Test;
  var
    Caller: Pointer;
    Info: TELLocationInfo;
  begin
    Caller := TEurekaStackList.Caller;
    Info := GetLocationInfo(Caller);
    Label1.Caption := Info.UnitName + '.' + Info.ClassName + '.' + Info.ProcedureName;
    // With matching symbols, this identifies the routine that called Test.
  end;
begin
  Test;
end;

Use EBaseClasses for TELLocationInfo.

You can also pass EurekaCallStackClass.Caller to the stack constructor as its first address. That chooses a starting code location, not ownership of any exception information object. It does not establish the assignment semantics of TEurekaExceptionInfo.CallStack (section 8.3).

11.3 Choosing the tracer

EurekaCallStackClass: TEurekaCallStackClass = TEurekaStackList;   // ECallStack
DefaultTracerMask: Cardinal = TracerRawEurekaLogMask;             // ECallStack

EurekaCallStackClass is the stack class used "when you want to create stack tracing class to get call stack without knowing current preferences" - the default being TEurekaStackListV7 - and DefaultTracerMask is the tracing-method mask that class uses. The same warning is printed on both pages: when you change one of these variables, be sure to change the other as well. The pair is also the extension point for a custom stack class - the variable can be replaced "to use your own custom call stacks with all EurekaLog's routines" - and the documentation's verdict on that is not recommended. The customizing chapter's own list of extension points, dated "as of EurekaLog 7.0.02", names ECallStack.TracerMethodsClasses for the specific stack classes beside ECallStack.EurekaCallStackClass for the default one.

function GetTracer(const ATracerNo: Integer): TEurekaBaseStackList;         // ECallStack
function GetTracerClass(const ATracerNo: Integer): TEurekaCallStackClass;   // ECallStack

GetTracer returns a ready instance for the number you name - created and initialised, with the stack not built yet. GetTracerClass returns the class, which you then instantiate with Create. The numbers are TracerXYZ constants of the EStackTracing unit, and the only enumeration the documentation gives is by name: TracerRawEurekaLogV7, TracerRawEurekaLogV6, TracerRawEurekaLog, TracerRawJCL, TracerRawMadExcept, TracerLeaksRAW, TracerFramesEurekaLogV7, TracerFramesRecovered, TracerFramesEurekaLog, TracerFramesJCL, TracerLeaksFrame, TracerWindows, "any other TracerXYZ value".

Prefer GetTracer over GetTracerClass when selecting a tracer, and use EurekaCallStackClass.Create when you want the configured default rather than a particular implementation. Do not select a method by changing the mask arguments to Create or Build.

This function demonstrates the documented TracerWindows selection and includes the current instruction. Place it in a unit using ECallStack and EStackTracing; it returns text rather than touching a form, and frees the stack it creates:

function CaptureWindowsStack: string;
var
  CallStack: TEurekaBaseStackList;
begin
  CallStack := GetTracer(TracerWindows);
  try
    CallStack.Build(CallStack.GetCurrentInstruction);
    Result := CallStack.ToString;
  finally
    CallStack.Free;
  end;
end;

EStackTracing is the unit that "contains actual code that performs stack tracing"; its page says its code is not to be used directly - always use ECallStack.

public
constructor Create(const ATracerNo: Integer = TracerUndefined); reintroduce;   // ECallStack - TGenericStackList

TGenericStackList is the class for tracing methods EurekaLog does not own. The number passed to Create is kept in its Tracer property, which "holds ATracerNo argument, which was used in Create constructor"; its own documented list of tracer names is the third-party one (TracerRawJCL, TracerRawMadExcept, TracerLeaksRAW, TracerFramesJCL, TracerLeaksFrame, TracerWindows). The warning is unambiguous: "Do not use this class for EurekaLog stack tracing." Like GetTracer, this constructor builds nothing - GetTracer's own return value is "created and initialized, but call stack is not build yet" - so you call Build afterwards in either case.

11.4 Creating the list and building it

TEurekaBaseStackList has two constructors. The one that takes arguments both creates and traces.

public
constructor Create(
  const AFirstAddr: Pointer;
  const AStackFrame: Pointer = nil;
  const ATopOfStack: Pointer = nil;
  const AStartLevel: Integer = -1;
  const ALevelsNumber: Integer = -1;
  const ADebugDetails: TEurekaDebugDetails = [ddUnit, ddProcedure, ddSourceCode];
  const AThreadID: DWord = 0;
  const ARunningThread: Boolean = True;
  const AGetDebugInfo: Boolean = True;
  const AAllowedMethods: DWord = TracerInvalidMask;
  const AStackOffset: PtrUInt = 0;
  const AContext: Pointer = nil
); overload; virtual;
constructor Create; overload; virtual;

Create without arguments "will create object instance without building call stack" - you trace on request with Build - while Create with arguments "will create object instance AND build call stack", and that overload "is equal to Create + Build". The warning attached to the constructor is a hard one: do not create instances of TEurekaBaseStackList class. Use EurekaCallStackClass for the default class, or GetTracerClass for a specific one.

public
procedure Build(
  const AFirstAddr: Pointer = nil;
  const AStackFrame: Pointer = nil;
  const ATopOfStack: Pointer = nil;
  const AStartLevel: Integer = -1;
  const ALevelsNumber: Integer = -1;
  const ADebugDetails: TEurekaDebugDetails = [ddProcedure, ddUnit, ddSourceCode];
  const AThreadID: DWord = 0;
  const ARunningThread: Boolean = True;
  const AGetDebugInfo: Boolean = True;
  const AMethods: DWord = TracerInvalidMask;
  const AStackOffset: PtrUInt = 0;
  const AContext: Pointer = nil
);

Build takes the same twelve arguments, one of them under a different name - AMethods here, AAllowedMethods in Create. Item by item:

Argument Documented meaning
AFirstAddr Code address to add to the stack; nil adds no additional items. When given: must point to a code location, not to the stack, and becomes the first item of the final call stack.
AStackFrame Where tracing starts; nil means use the current stack pointer, i.e. trace the entire stack. If given: must point to the stack, must be a valid stack frame, and must be less than ATopOfStack.
ATopOfStack Where tracing ends; nil means use the current stack limits. Usually left unspecified, "unless you want to build call stack for another (external) thread"; if given, must be greater than AStackFrame.
AStartLevel How many items (levels) to skip from the start; -1 means "do not remove any items".
ALevelsNumber Limits the length of the call stack; -1 means "do not limit call stack".
ADebugDetails Only store locations with this level of detail.
AThreadID A thread id, not a handle, marking the items as belonging to that thread; 0 means the current thread. A target thread "must be a current thread or a suspended thread" - and there is an easier route (section 11.5).
ARunningThread True "when build call stacks manually"; False is used to indicate an error (exception) thread.
AGetDebugInfo False retrieves only RAW pointers, leaving UnitName, ClassName, ProcedureName and LineNumber empty; True fills every possible property - "This works slower."
AMethods / AAllowedMethods Mask of tracing methods; the description names DefaultTracerMask ("which is 'EurekaLog 7 RAW'") as the default, then instructs: "Do not use this argument. Use GetTracer or GetTracerClass functions."
AContext "A pointer to CPU context record. Supply this value when available."
AStackOffset No public semantics are specified - see section 11.9.

Both constructors are virtual; the published Build declaration has no such modifier. For an ordinary current-thread capture, the simpler documented entry point is GetCurrentCallStack. Place this function in a unit using ECallStack:

function CaptureCurrentStack: string;
var
  CallStack: TEurekaBaseStackList;
begin
  CallStack := GetCurrentCallStack;
  try
    Result := CallStack.ToString;
  finally
    CallStack.Free;
  end;
end;

This is a snapshot taken when the helper runs, not the saved stack of a previous exception. No full public declaration is specified for GetCurrentCallStack; use the call form above. For a single caller location, avoid allocating a whole stack and use section 11.2.

11.5 A thread's own stack, and another thread's stack

Building your own stack is "manual" mode and uses ARunningThread = True. Building a stack for a thread that is not the caller passes that thread's id, its stack bounds and, ideally, its CPU context: ATopOfStack is documented as usually specified only in this case, AThreadID as marking the items as belonging to the specified thread, and AContext as the CPU context record to supply when available. The documentation warns that the target thread must be the current thread or a suspended thread, and points at the simpler entry point - "if you want to build a call stack for another (external) thread - there is an easier way: use the BuildForThread method". BuildForThreadByID is listed beside it.

BuildForThread is the higher-level entry point for an on-demand background-thread snapshot. The following adapts the documented call form into a function returning text. Place it in a unit using Winapi.Windows, ECallStack, EStackTracing and ETypes (Windows on older Delphi).

The caller must supply a valid handle and matching ID for a different live thread. The handle needs THREAD_GET_CONTEXT, THREAD_QUERY_INFORMATION and THREAD_SUSPEND_RESUME rights. Do not pass GetCurrentThread or GetCurrentThreadId; use the current-thread helper instead. Keep the target and handle valid for the duration of the capture. This function does not own or close that supplied handle.

function CaptureWorkerStack(const AThreadHandle: THandle;
  const AThreadID: DWORD; out WasSuspended: Boolean): string;
var
  CallStack: TEurekaBaseStackList;
begin
  CallStack := EurekaCallStackClass.Create;
  try
    CallStack.BuildForThread(AThreadHandle, AThreadID,
      nil,                                // no additional first address
      [ddUnit, ddProcedure, ddSourceCode], // requested detail levels
      True,                               // resolve debug information
      False,                              // do not show begin calls
      TracerRawEurekaLogV7Mask,             // documented explicit tracer choice
      True,                               // running-thread capture
      @WasSuspended);
    Result := CallStack.ToString;
  finally
    CallStack.Free;
  end;
end;

WasSuspended reports whether the thread was suspended before the capture. It is not a promise that the thread remains stopped afterwards: the background thread continues to run, so its live stack can already differ from your snapshot. Use the returned text on the UI thread if you want to display it; collecting it does not grant permission to access a VCL control from a worker.

The parameterless constructor avoids tracing the current thread only to replace that stack immediately. BuildForThreadByID is the variant that needs no handle, but no full public declaration is specified for it. Use the BuildForThread call above rather than guessing arguments for that variant. For a full error report about one thread rather than a stack string, use RaiseFreezeException (chapter 14).

There is also a passive route: turn on "Capture stack of RTL threads" or "Capture stack of Windows threads" and background threads' stacks arrive inside the same .CallStack property of the exception information object (chapter 8) - the exception thread's items first, the optional calling thread's after them, the items from other threads distinguished by their .ThreadID, and the exception's own address as the property's first item. Only the exception thread's stack is captured by default, and the price of the rest is stated plainly: capturing another thread requires suspending it, which "can cause deadlock issues (for example: thread may be suspended when it is running memory allocation function; thus, any further memory alloc/release operation will block application forever). Do not enable this option until really needed."

11.6 From address to location

function GetLocationInfo(const Addr: Pointer): TELLocationInfo; overload;               // EDebugInfo
function GetLocationInfo(const Addr: Pointer; var Info: TELLocationInfo): Boolean; overload;
function GetLocationInfoStr(const Addr: Pointer): String; overload;                     // EDebugInfo
function GetLocationInfoStr(const Addr: Pointer; out Info: String): Boolean; overload;

The first GetLocationInfo overload returns the record; the second writes it through Info and returns "success / failure state". Inside, "EurekaLog will load debug information (if any available) and look it up for the specified address" - the input is "a pointer to a code (method, function, line, etc.)", the output the human-readable description: unit, class, routine, line, offsets. GetLocationInfoStr is the same lookup as a string, "a simple combination of GetLocationInfo and LocationToStr functions", and the documentation gives the result's shape as an example worth reading symbol by symbol:

(003BDD73){Project1.exe} [007BDD73] Unit1.TForm1.Button1Click (Line 32, "Unit1.pas") + $5

Read it against the formatter output in section 11.8: (003BDD73) is the offset from the start of the module and [007BDD73] the absolute address (Offset = Address - Module, and the two columns carry exactly these two values), {Project1.exe} the module, then unit.class.routine, the line and source file, and finally + $5 the offset from the routine.

function LocationToStr(
  const AInfo: TELLocationInfo;
  const AIncludeModuleName: Boolean = True;
  const AIncludeAddressOffset: Boolean = True;
  const AIncludeStartProcLineOffset: Boolean = False;
  const AIncludeVAddress: Boolean = True;
  const AIncludePointer: Boolean = True;
  const AIncludeLine: Boolean = True;
  const AUseParent: TELUseParent = eupNone
): String;

It "converts location information record into single string, which can be shown to user". Every switch is documented: AIncludeModuleName the module name, AIncludeAddressOffset the offset from the routine, AIncludeStartProcLineOffset the offset from the line (the one that defaults to False), AIncludeVAddress the offset from the module, AIncludePointer the RAW pointer, AIncludeLine the line number, and AUseParent the information about parent or child units - "for includes only". The five-False call used throughout the documentation's examples therefore leaves exactly the human part: 'Unit1.TForm1.Button1Click (Line 42, "Unit1.pas")'.

The location record

GetLocationInfo fills a record of the EBaseClasses unit, "A code location information", with a pointer type beside it:

PELLocationInfo = ^TELLocationInfo;   // EBaseClasses

Its fields answer, one by one, the questions the report's call stack columns ask:

Field Type What it holds
Address Pointer RAW pointer - "the original address to code which was used to obtain information".
Module HMODULE Start of the module; the .exe, .dll or .bpl the code belongs to.
ModuleName String The module name, as a full absolute file name.
ModuleType TEurekaModuleType Kind of module - .exe, .dll or .bpl.
Offset DWORD = Address - Module: offset from the start of the module, in bytes.
UnitName String Unit symbol name (not file name), for example Unit1.
SourceName String File name without path, for example Unit1.pas - "Unit file name (not unit name!)".
ClassName String Class symbol name of the method the code belongs to; empty outside a method.
ProcedureName String Method name when ClassName is not empty; otherwise the function or procedure name.
LineNumber Integer Number of the text line the code belongs to.
OffsetFromProcName Integer = Address - StartOfRoutine, in bytes.
OffsetFromLineNumber Integer = Address - StartOfLine, in bytes.
ProcOffsetLine Integer = LineOf(Address) - LineOf(StartOfRoutine), in lines.
DebugDetail TEurekaDebugDetail Level of detail; "it indicates what fields in this record are filled".
DebugInfo TELDebugInfoSource The provider instance "which was used to load debug information".
BinaryFileName String Full absolute name of the file the debug information came from. "Typically it is the same as ModuleName. It will be different if debug information was loaded from external file (such as .map, .jdbg, etc.)."
ParentSourceName String Declared in the record; not described.
ParentLineNumber Integer Declared in the record; not described.
ParentProcOffsetLine Integer Declared in the record; not described.
ParentOffsetFromLineNumber Integer Declared in the record; not described.

No public semantics are specified for the four Parent* fields. The include-file handling in section 11.7 defines a parent/child distinction, but does not establish a contract for those fields. Address and Module are the address the lookup was asked about and the module that contains it - GetLocationInfo(@Test) fills both without any stack being traced. DebugInfo and BinaryFileName say where the symbols came from; DebugDetail says how much of the record was filled.

Where the symbols come from

DebugInfo's type, TELDebugInfoSource (= class(TCheckedInterfacedObject)), is the base class for debug information providers - "a class which can load and parse debug information in one specific format. Debug info provider can be registered in EDebugInfo unit" (RegisterDebugInfoSource is the registration entry the customizing chapter names). Child classes "must override at least InitializeSource and GetLocationInfo methods. Most providers will also override Heuristic method." The providers are separate units, not part of EDebugInfo itself: that unit "does not implement any support for debug information by itself", so each format you want has to be included as well - EDebugMAP for .map files, EDebugJCL for JEDI, and so on - and more than one can be included at a time. One provider ships per format, each in its own unit:

Two warnings a deployment needs. The DbgHelp-based provider "Requires these DLLs to be present/deployed", and "DbgHelp is a single thread DLL, which means routing all debug info requests to a single thread. Which can cause a lot of slow down in multi-threaded applications. We don't recommend to use it on production. Consider converting debug information to some other supported format." The export-table providers are marked "Heuristic." - that is how they find a name, guessed from what the module exports, not how precise the result is described as being. A missing answer is described by a different word, the detail level ddModule: "Code belongs to module, but module does not have debug information, or code lies outside of debug information scope. Only module info is available."

Detail levels

The ADebugDetails argument of Create and Build and the DebugDetail field of the record come from the same pair of types - the argument is the set, the field is one member of it:

TEurekaDebugDetails = set of TEurekaDebugDetail;   // ETypes
TEurekaDebugDetail = (ddNone, ddModule, ddUnit, ddProcedure, ddSourceCode);   // ETypes
Level Meaning
ddNone No debug information is available.
ddModule Module exists but has no debug info, or the code lies outside its scope; only module information is available.
ddUnit Unit name is available.
ddProcedure Routine name is available.
ddSourceCode Line number is available.

AGetDebugInfo = False stops the whole lookup - raw pointers only - while ADebugDetails is the filter that decides which levels are worth storing. The documentation's examples pass [ddUnit, ddProcedure, ddSourceCode], and a custom formatter used in the customizing chapter filters stack items with Stack.Items[I].Location.DebugDetail in [ddUnit..ddSourceCode] - the range form that the published declaration order makes valid.

11.7 Where you are standing: the six helpers

The EDebugInfo unit also answers the current-location question without a pointer. Each helper combines GetLocationInfo and GetCaller to describe the place it is called from.

function __LOCATION__: String;   // EDebugInfo - for example 'Unit1.TMyClass.DoSomething 42'
function __MODULE__: String;     // EDebugInfo - for example 'Project1.exe'
function __UNIT__: String;       // EDebugInfo - for example 'Unit1'
function __FUNCTION__: String;   // EDebugInfo - for example 'DoSomething', or 'TMyClass.DoSomething'
function __FILE__: String;       // EDebugInfo - for example 'Unit1.pas', 'Unit1.cpp', 'Code.inc'
function __LINE__: Integer;      // EDebugInfo - for example 42

__LOCATION__ returns "human-readable description of the current location", __MODULE__ the executable module name, __UNIT__ the unit symbol name, __FUNCTION__ the routine name (class-prefixed for methods), __FILE__ the source file name, and __LINE__ the line number - the only one of the six that is not a string. For example, the following expression belongs at the point you want to log, in a unit using EDebugInfo and System.SysUtils:

'Entering ' + __FILE__ + ' ' + __FUNCTION__ + ' ' + IntToStr(__LINE__)

Pass that expression to your logger (chapter 13). The explicit conversion is required: __LINE__ returns an integer, not text.

Two of the six carry notes about include files and generics, saying the same thing from different directions. __LINE__: the line number "belongs to source code, not the unit. While these are the same most of the time, it is not always so. For example, if you add an include file with code, then unit name will be something like 'Unit1', while line numbers will be from Code.inc for all code inside the include file. The same is true for generics." __FILE__ makes the mirror-image observation about the source name, with 'Code.inc' and 'System.Generics.Collections.pas' as examples. Prefer __FILE__ when the consumer needs the actual source file containing the line rather than the compiled unit's name.

The same include-file problem is what LocationToStr's AUseParent argument addresses; its value type is documented by three identifiers of the EDebugInfo unit:

Identifier Documented meaning
eupNone Use debug information for the non-include file.
eupParent Use debug information for the file with {$I} directive.
eupChild Use debug information for the included file.

11.8 Turning a stack into text

A TEurekaBaseStackList renders itself, and the documentation gives five options: E.StackTrace (Delphi 2009+) for an exception's stack as a string; CallStack.ToString for the default formatting as one string; SomeStrings.Assign(CallStack) - the destination receives the stack - for the same into a TStrings; and then CallStackToString and CallStackToStrings from the ECallStack unit, which take the stack, an optional header caption and an optional formatter in place of the default one - the first returns the text, the second writes it into the TStrings you name, and a formatter you construct is yours to free. The documented call forms are CallStackToString(CallStack), CallStackToString(CallStack, 'Error Details:') and CallStackToString(CallStack, '', Formatter), and the same three for CallStackToStrings with the destination TStrings as the second argument (CallStackToStrings(CallStack, Memo1.Lines), and so on).

These are formatting call forms, not standalone programs. They assume a live stack and, for CallStackToStrings, a live destination TStrings from System.Classes. A Memo1.Lines destination is appropriate only on that control's UI thread. If the stack came from TEurekaExceptionInfo.CallStack, do not apply the creator-owned .Free pattern from the capture helpers above: the property does not document transfer of ownership to your code.

Four formatters are built in: TEurekaStackFormatter (the general fixed-width table with columns, "best to be used in text files"), TEurekaStackFormatterV6 (the EurekaLog V6 format, "less columns"), TSimpleStackFormatter (list-like, no columns, for variable-width fonts and message boxes), and TCompactStackFormatter (the same, more compact and with less details - "good for quick preview"). The general formatter's output looks like this:

|Methods |Details|Stack   |Address |Module        |Offset  |Source          |Unit        |Class        |Procedure/Method|Line   |
|7FFFFFFE|04     |00000000|007CD41D|Project183.exe|0048D41D|Unit1.pas       |Unit1       |TForm1       |Button1Click    |37[1]  |
|00000060|04     |019DF02C|006F5043|Project183.exe|003B5043|Vcl.Controls.pas|Vcl.Controls|TControl     |Click           |7707[9]|

The Address and Offset columns are TELLocationInfo.Address and .Offset of section 11.6; the unit, class, procedure, source and line columns are the same record's symbol fields. The full table also carries a header block above the rows, one per thread, giving ID, Parent, Priority, Class, Name, DeadLock and Wait Chain. The compact formatter reduces each row to an address and a location line: [0132D41D] Unit1.TForm1.Button1Click (Line 37, "Unit1.pas").

Formatters can be replaced, too: the customizing chapter shows a class descending from TEurekaBaseStackFormatter overriding GetItemText and GetStrings, walking the stack with Stack.Count, Stack.Items[I], Stack.Delete and Stack.Assign, and reading each item through CallStack.GetItem(AIndex, Cache) and CallStack.ComposeName(ClassName, ProcedureName) plus .Location and .ThreadID; the item type it names in its signature is TEurekaDebugInfo (with PEurekaDebugInfo as the pointer it receives), and the formatter copies the original stack's Formatter and sets its own CaptionHeader.

11.9 Contract limits

  • No public semantics are specified for AStackOffset. The tracing Create and Build declarations give it the default 0; do not infer its behavior from the name.
  • The tracing mask's default has two descriptions. The declarations give TracerInvalidMask, while the parameter contract identifies DefaultTracerMask ("EurekaLog 7 RAW"). Their relationship is not specified. Do not set this argument directly; use GetTracer or GetTracerClass. The two ADebugDetails defaults, in contrast, differ only in the order of the same three levels: [ddUnit, ddProcedure, ddSourceCode] on Create, [ddProcedure, ddUnit, ddSourceCode] on Build.
  • Full public declarations are not specified for GetCurrentCallStack, CallStackToString, CallStackToStrings, Caller, BuildForThread or BuildForThreadByID. Use the documented call forms in this chapter rather than guessing additional parameters or overloads.
  • No public item-layout contract is specified for TEurekaDebugInfo or PEurekaDebugInfo. The stack operations Count, Items, Delete, Assign, ToString and the formatter operations described above do not establish a complete contract for extending those internals.
  • ETypes.PStackFrame is given as the return type of GetCurrentStackFrame and described only as pointing to stack data "which points to current stack frame".
  • DWARF and Stabs providers are documented as "Not implemented yet", and the DbgHelp-based one with a recommendation against production use in multi-threaded applications.

For ordinary applications, these gaps are not reasons to write a custom tracer or provider. Use the documented capture forms, retain matching symbols, and inspect the reported detail level. If the report still lacks usable frames, follow the build/symbol workflow in chapter 2 and the diagnostic sequence in chapter 5 before replacing tracing internals.