EurekaLog
Menu

The EurekaLog Developer Guide

10. EBase: state, exceptions and threads

EBase is the small integration surface for code that needs EurekaLog state, exception handling or thread helpers. It can be included without pulling in the entire EurekaLog implementation, which makes it useful in shared code that also builds without reporting enabled.

Chapter 6 covers its everyday calls. This chapter fills in the boundary cases: decoding a send response, turning an exception into an error code, and choosing how a worker's fatal exception is processed. It also introduces EBaseClasses, whose location record is used by the symbol lookup APIs in chapter 11.

Keep three outcomes separate: a BugID groups reports, a sender's issue ID identifies a tracker entry, and an exception error code tells a caller why an operation failed. None substitutes for the others.

10.1 The shape of the unit

Use IsEurekaLogInstalled and the activation functions when the decision is made at run time. Use the documented EUREKALOG / EUREKALOG7 conditional symbols when the decision belongs at compile time. Importing EBase alone is not proof that the executable was processed by EurekaLog; chapter 6 explains the installation check.

The unit groups naturally by task:

Types used by these routines may belong elsewhere: TResponse, for example, is in ETypes. EBaseClasses is a separate unit containing TELLocationInfo, its pointer type PELLocationInfo and the TELDebugInfoSource provider base class (section 10.6).

10.2 The routines chapter 6 already documents

These are the entries of chapter 6 that come from this unit, and the chapter to read for each signature, parameter, default and warning:

One comparison is worth carrying into the next section: LastBugID (chapter 6) gives you the report's own BugID for the current thread, while the send-response routines in section 10.3 report two things about the send itself - the error code of a failure, and the ticket the report became. What follows is the part of the unit chapter 6 leaves out.

10.3 Outcomes you hand back to a caller

The three routines in this section answer one question in three currencies: what do I tell the code that called me? Two convert the response of a send into something a person or a log can use; the third converts an exception into the numeric result that a caller outside your code expects.

ErrorCodeToStr

function ErrorCodeToStr(const AResponse: TResponse): String;   // EBase

Turns a send response into a human-readable error code. The single parameter is a response from sending, and the return value is the readable form - the reference's own example of a result is the string '42'.

Why the conversion is needed at all is the useful part of the page: when sending fails, the response encodes an error code for the failure, and different senders use different formats of error codes, so the encoded value has to be decoded by something that knows the sender's convention. The function returns an empty string when there is no error code - and the reference attaches a warning to that: a missing error code is not the same as a successful send, because a send can fail without providing an error code back. If what you need is to know whether the send failed, the page's own instruction is to test the result field. This expression assumes Response: ETypes.TResponse has already been returned by the operation, and Failed is available from the Windows API unit:

Failed(Ord(Response.SendResult))

A true result means the send failed, whether or not ErrorCodeToStr(Response) contains text.

Use it where the failure has to be shown or logged with the sender's own reason - in your own send wrapper, or in an event handler that inspects a send that EurekaLog has already performed.

IssueIDToStr

function IssueIDToStr(const AResponse: TResponse): String;   // EBase

The same conversion for the other half of the outcome: the response from a send is also where the tracker's ticket reference lives, and this function returns it as a human-readable ID of the posted ticket/issue - the reference's example value is again '42'.

The page's scope note is more important than the signature. The function returns an empty string when the response comes from a sending method that does not support feedback - the examples given are e-mail sending and FTP sending. So an empty result is what a method without feedback produces; it is not a verdict on whether the report arrived.

Use it when you want to tell the user or your own log which ticket their report became. It is the tracker's identifier from the response, and a different thing from the report's own BugID (chapter 6's LastBugID).

GetExceptionErrorCode

function GetExceptionErrorCode(const AExceptObject: TObject): Cardinal;   // EBase

Converts an exception into a Cardinal value that can be returned to a caller - possibly through SetLastError. The page describes it as a helper for returning a value as GetLastError or as the exit code of a thread or a process, and the return value is exactly that: a number to hand back, not a message.

Pass the exception object while it is valid, normally ExceptObject inside the catch boundary. The parameter has no separately documented nil behavior; do not use nil to mean success.

Watch the signedness at the boundary. GetExceptionErrorCode returns Cardinal; older Delphi declarations of SetLastError/GetLastError use Integer, while modern ones use Cardinal. Delphi exit codes can likewise be Integer where Windows uses Cardinal. Cast explicitly when the receiving API requires it.

Here is a complete, deliberately small Delphi-side boundary adapter. Place it in a unit using Winapi.Windows, System.SysUtils and EBase (Windows/SysUtils on older Delphi). It converts an integer string and exposes a conversion exception as a last-error code. It is not a declaration of a language-neutral DLL ABI: exported interfaces must separately agree on parameter and result types with their callers.

function ParseIntegerOrLastError(const Text: string; out Value: Integer): Boolean; stdcall;
begin
  Value := 0;
  try
    Value := StrToInt(Text);
    Result := True;
  except
    SetLastError(GetExceptionErrorCode(ExceptObject));
    Result := False;
  end;
end;

On a False result, the caller must read GetLastError before another call overwrites it. A successful call does not clear last error. For an older declaration that takes Integer, use SetLastError(Integer(GetExceptionErrorCode(ExceptObject))).

This adapter returns failure; it does not itself request a EurekaLog report. For a safecall boundary, use the Exception2HRESULT contract in chapter 16 instead.

10.4 Threads

Threads are where the unit's three concerns meet. A thread has its own EurekaLog state (chapter 6's SetEurekaLogStateInThread), its own last exception (which is what LastBugID reads), and, if you want EurekaLog to process what it did not catch, a way of handing that exception over. EBase contributes one class and one more routine to this.

TThreadEx

TThreadEx is EurekaLog's abstract replacement for TThread, offering two extra constructor arguments that control the thread's name and EurekaLog's state in it:

TThreadEx = class(TThreadHook);   // EBase

No public contract is specified for the intermediate TThreadHook class. Use TThreadEx through its documented constructor, Execute and AutoHandleException members rather than relying on that parent's internals. The added controls are a name, per-thread EurekaLog state and optional automatic processing of the thread's fatal exception.

TThreadEx is the class-side counterpart of BeginThreadEx. For a new thread that needs this integration, derive it from TThreadEx instead of TThread.

For Delphi versions supporting anonymous methods and thread Start, the documented anonymous factory takes a thread body and a name. This procedure is a deliberate failure demonstration, not background application work. Place it in a unit using EBase and System.SysUtils, in a project with EurekaLog installed and active:

procedure StartReportingTestThread;
var
  Thread: TThreadEx;
begin
  Thread := TThreadEx.CreateAnonymousThread(
    procedure
    begin
      raise Exception.Create('Deliberate worker reporting test');
    end,
    'Reporting test');
  Thread.AutoHandleException := True;
  Thread.Start;
  Thread := nil;
end;

Anonymous threads are FreeOnTerminate by default: configure the object before Start and never dereference that variable afterwards. This example uses automatic handling; it does not also install an OnTerminate handler that reports the same exception.

TThreadEx.Create

public
constructor Create; overload;
constructor Create(CreateSuspended: Boolean); overload;
constructor Create(const AName: String; const AELEnabled: Boolean = Boolean(2)); overload;
constructor Create(CreateSuspended: Boolean; const AName: String;
  const AELEnabled: Boolean = Boolean(2)); overload;

Four overloads: two that differ only in CreateSuspended, and two that add the thread's name and EurekaLog's state in it.

  • CreateSuspended [optional]: if False, Execute is called immediately after the constructor; if True, it is not called until Start is. Default is False.
  • AName [optional]: the thread's name - arbitrary text, which the page says is visible in the IDE's debugger and is used by EurekaLog in bug reports. Default is an empty name, meaning no name, with the thread id used to identify the thread.
  • AELEnabled [optional]: EurekaLog's status in the created thread. Two statements come with it: the argument is ignored if you compile your application without EurekaLog, and the default is the same state as the current thread.

The declared default Boolean(2) means "same state as current thread" for this parameter. BeginThreadEx uses the same default (chapter 6); do not interpret it as an ordinary Boolean value or infer other behavior from its numeric representation.

The following constructor fragments assume a declared TMyThread = class(TThreadEx) with an Execute implementation and a local Thread: TMyThread. They illustrate overload selection, not a complete ownership/shutdown policy; use them inside the routine that owns that policy:

// Create thread suspended, then start it:
Thread := TMyThread.Create(True);
// ...
Thread.Start;

// Name and execute thread immediately:
Thread := TMyThread.Create('My background thread');

// Create named thread suspended:
Thread := TMyThread.Create(True, 'My background thread');
// ...
Thread.Start;

// Name, and disable EurekaLog in this thread:
Thread := TMyThread.Create('Thread Name', False { disable EurekaLog in thread });

One sentence in those examples is worth separating from them, because it reads slightly differently from the parameter's own default: "Threads launched with TThreadEx class will be EurekaLog-enabled by default." The constructor's description says the default is the current thread's state, which is not the same promise - a thread created from a thread where EurekaLog is disabled inherits a disabled state. If the distinction matters in your application, pass the argument explicitly rather than relying on either reading.

TThreadEx.Execute

protected
procedure Execute; reintroduce; virtual;

The method your thread's work goes into. The page's instructions are the RTL's, unchanged: override Execute and insert the code that should run; Execute is responsible for checking Terminated to decide whether the thread needs to exit; and it runs when Create is called (if CreateSuspended is False) or when Start is called for the first time.

The one instruction the page marks as important is the difference from a plain override: you must call the inherited implementation in your overridden Execute - the reason being that TThreadEx's own Execute is what EurekaLog's behaviour in the thread hangs off. The following is a class/body template in a unit using EBase, not a working task: replace the body comment with the thread's work and check Terminated in any long-running loop.

type
  TMyThread = class(TThreadEx)
  protected
    procedure Execute; override;
  end;

procedure TMyThread.Execute;
begin
  inherited; // - this is required
  // ... your code ...
end;

TThreadEx.AutoHandleException

Set AutoHandleException := True before starting the worker when its fatal exception should invoke HandleException automatically. The default is False: standard thread behavior stores the exception in FatalException, leaving your code to examine it. Merely using TThreadEx does not select automatic reporting of that fatal exception.

The page is careful about what does not change: even with AutoHandleException set to True, the FatalException property is still filled with the exception. So the switch adds the handling, it does not replace the record of what went wrong.

If you deliberately choose manual re-raising, the following fragment assumes Thread is a live TMyThread instance with FreeOnTerminate = False, E is a local TObject, and the caller frees the thread in an enclosing finally. It is the reference's low-level transfer pattern, not code to apply to a self-freeing thread:

// Wait for thread's completion, then re-raise any thread error in current thread.
// You should do this only after the thread has finished.
Thread.WaitFor;

E := Thread.FatalException;
if Assigned(E) then
begin
  // clear FatalException property
  PPointer(@Thread.FatalException)^ := nil;
  raise E;
end;

Clearing FatalException is essential to this particular re-raise pattern: the thread must not retain ownership of the object being raised elsewhere. The pointer write bypasses the property interface and is version-sensitive; prefer automatic handling if re-raising in the caller is not a requirement.

Alternatively, an OnTerminate handler can test Assigned(Thread.FatalException) and call EBase.HandleException(Thread.FatalException, False, atThread) without taking ownership. The handler needs EBase and the atThread identifier from ETypes. For automatic processing, the following startup procedure uses the TMyThread descendant declared above:

procedure StartWorker;
var
  Thread: TMyThread;
begin
  Thread := TMyThread.Create(True, 'My thread', True);
  Thread.AutoHandleException := True;
  Thread.FreeOnTerminate := True;
  Thread.Start;
  Thread := nil; // do not dereference a self-freeing thread after Start
end;

Here the constructor explicitly enables EurekaLog for the worker; the project must still have EurekaLog installed and globally active. Replace the template Execute with actual work before using this startup sequence. FatalException remains populated after automatic processing, but a self-freeing thread is not an object you can safely inspect later. Chapters 15 and 21 cover the wider thread lifetime and synchronization rules.

NameThreadForDebugging

procedure NameThreadForDebugging(const AName: String; const AThreadID: Cardinal = 0);   // EBase

An alias for NameThread (chapter 6), documented as overriding System.Classes.TThread.NameThreadForDebugging. The parameters are the thread's name and, optionally, the thread id. Pass an explicit id when you mean a thread other than the one you are in.

Use this alias when you want EurekaLog's naming implementation with the familiar RTL-style name. Its behavior is the same as NameThread (section 6.4), including EurekaLog's support for GetThreadDescription/SetThreadDescription. Differences from the RTL are version-sensitive; do not assume every later RTL release has the same limitations.

10.5 The error class: EEurekaLogGeneralError

EEurekaLogGeneralError = class(Exception);   // EBase

The base exception class for EurekaLog's own exceptions, and the only class in EBase other than TThreadEx. Its page gives two statements: that it is the base of EurekaLog's exceptions, and what it is for - use it in exception filters or exception events to filter out EurekaLog-related exceptions. That is the whole of the documented purpose: the class exists so that your code can tell "an error in my program" from "an error about EurekaLog", which otherwise arrives through the same hooks and looks the same. Chapter 7 covers the events and filters this refers to.

There is a second use that comes from the exception manager rather than from this page: the manager's StandardEurekaError creates an exception of this class and passes it to Handle, and the manager's reference recommends the class precisely so you can differ such an "exception" - invoked without raising anything - from a typical one. Chapter 8 documents that from the manager's side.

Do not treat this class as proof of an internal EurekaLog failure: your own call to StandardEurekaError also creates it. Classify using the operation and context as well as the type.

10.6 EBaseClasses: the location record and its provider base class

EBaseClasses supplies the location record, its pointer type, and the provider base class. Providers turn an address into the unit, routine and line that chapter 6's GetLocationInfo and LocationToStr return.

TELDebugInfoSource

TELDebugInfoSource = class(TCheckedInterfacedObject);   // EBaseClasses

The base class for debug information providers, each of which loads and parses one specific debug-information format. Its extension contract is limited:

  • register a provider through EDebugInfo.RegisterDebugInfoSource (chapter 24);
  • child classes must override at least InitializeSource and GetLocationInfo;
  • most providers will also override Heuristic;
  • the parent class is TCheckedInterfacedObject; no additional parent-class contract is specified.

No public override signatures are specified here, so this is not a complete recipe for implementing a new provider. That limitation does not block normal symbol lookup: use one of the supplied providers listed in section 11.6. They cover EurekaLog native formats, MAP, TD32, JDBG, mORMot, madExcept compatibility, Microsoft symbols and export-table heuristics, each under its documented limitations. DWARF and Stabs are named classes but are explicitly marked "Not implemented yet"; do not select them as working format support.

TELLocationInfo

TELLocationInfo = record            // EBaseClasses
  Address: Pointer;
  DebugDetail: TEurekaDebugDetail;
  Offset: DWORD;
  Module: HMODULE;
  ModuleName: String;
  ModuleType: TEurekaModuleType;
  UnitName: String;
  SourceName: String;
  ClassName: String;
  ProcedureName: String;
  LineNumber: Integer;
  OffsetFromProcName: Integer;
  ProcOffsetLine: Integer;
  OffsetFromLineNumber: Integer;
  ParentProcOffsetLine: Integer;
  ParentLineNumber: Integer;
  ParentOffsetFromLineNumber: Integer;
  ParentSourceName: String;
  DebugInfo: TELDebugInfoSource;
  BinaryFileName: String;
end;

One code location, as a record of twenty fields. No public semantics are specified for the four Parent* fields; do not infer what fills them. The remaining sixteen fields answer four questions.

Which code, in which module. Address is the absolute code address - the original address used to obtain the information. Module is the executable module (.exe, .dll, .bpl) the code belongs to, given as the start of that module; ModuleName is its full absolute file name, and ModuleType says which kind it is - exe, dll or bpl. Offset is Address - Module, the offset from the start of the module in bytes.

Where the answer came from. DebugInfo is the class that loaded the debug information (an instance of TELDebugInfoSource or one of its descendants); BinaryFileName is the file it was loaded from, normally the same as ModuleName and different when the information came from an external file such as a .map or .jdbg. DebugDetail - of type TEurekaDebugDetail, declared by ETypes - is the level of detail, and therefore the field to read before trusting the rest: the page says it indicates which fields in this record are filled. A record with a low level of detail is not one whose other fields are wrong; it is one whose other fields may be empty.

Where in the source. UnitName is the unit symbol name, such as Unit1; SourceName is the unit's file name without its path, such as Unit1.pas, and the page is emphatic that neither is the other - one is "not file name", the other "not unit name". LineNumber is the text line the code belongs to. Chapter 6 draws the same unit-versus-file distinction for __UNIT__ and __FILE__.

Where in the routine. ClassName is the class symbol name the code belongs to, and it is empty when the code is not inside a method - which is the signal to read ProcedureName on its own terms: a method name when the class is not empty, a function or procedure name when it is empty. OffsetFromProcName is Address - StartOfRoutine, the offset from the start of the routine in bytes; ProcOffsetLine is LineOf(Address) - LineOf(StartOfRoutine), the difference in line numbers between the code and the start of its routine; OffsetFromLineNumber is Address - StartOfLine, the offset from the start of the line in bytes.

These are exactly the pieces chapter 6's LocationToStr turns into a line of text: its switches select the module name, the offset from the routine, the offset from the line, the offset from the module, the raw pointer and the line number, each of them a field above - and this is the record a lookup in chapter 11 hands back.

PELLocationInfo

PELLocationInfo = ^TELLocationInfo;   // EBaseClasses

A pointer to code location information: use it where a location record is passed by reference rather than by value.