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:
- State: five global/per-thread query and change functions.
- Exceptions:
HandleException, message accessors, expected-exception helpers and chain builders. - Threads:
BeginThreadEx,EndThreadEx,NameThread,NameThreadForDebuggingandTThreadEx. - Results:
ErrorCodeToStr,IssueIDToStrandGetExceptionErrorCode. - Classification:
EEurekaLogGeneralError, the base for EurekaLog's own exception classes.
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:
- State:
IsEurekaLogInstalled,IsEurekaLogActive,IsEurekaLogActiveInThread,SetEurekaLogState,SetEurekaLogStateInThread- the build-time question, the run-time one, its per-thread version, and the two setters, which return the previous state so that a cleanup path can restore it rather than assume it. Chapter 6, section 6.1. - Handing an exception over:
HandleException(with its fallback path when EurekaLog is not installed),ExceptionMessageandSetExceptionMessage- the getter, and the setter that exists because EurekaLog records a message the first time it sees an exception. Chapter 6, section 6.2 - and the exception manager's own shortcuts, covered in chapter 8. - Expected exceptions and chains:
MarkExpected,RaiseExpected,ShowExpected,IsExpected, and the chain buildersRaiseOuterExceptionandRaiseOuterExceptionExpected. Chapter 6, section 6.2. - Threads:
BeginThreadEx, the replacement for the RTL'sBeginThread, with the thread name and EurekaLog's state in the new thread as extra arguments;EndThreadEx, which stands in forEndThreadinside a thread created that way and carries a documented warning against terminating a thread abruptly instead of leaving its procedure normally; andNameThread, whose own recommendation is to use EurekaLog's naming rather than the RTL's. Chapter 6, section 6.4.
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]: ifFalse,Executeis called immediately after the constructor; ifTrue, it is not called untilStartis. Default isFalse.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
InitializeSourceandGetLocationInfo; - 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.