While event handlers are simple callback procedures, there are few potential pitfalls that you need to be aware of.
Registering an event handler
You have a choice of registering an event handler:
1. with VCL TEurekaLogEvents component:
or:
2. by calling code from EEvents unit:
uses
EEvent;
initialization
RegisterEventExceptionNotify(nil, MyHandler);
end.
What is the difference?
Event handlers registered via TEurekaLogEvents component will be active only when corresponding form is live (created). Such handlers will not be called before form is created or after it was destroyed. For this reason you may want to register your event handlers via code. For example, it would be a bad idea to use TEurekaLogEvents component for implementing your error dialog, because your dialog will not be displayed for exceptions outside form (e.g. initialization/finalization exceptions, as well as exceptions in form's constructor).
In other words:
uses
EEvents;
// Assigned to OnExceptionNotify event of TEurekaLogEvents component on the form
procedure TForm1.EurekaLogEvents1ExceptionNotify(
AExceptionInfo: TEurekaExceptionInfo; var AHandle, ACallNextHandler: Boolean);
begin
// ...
end;
// Not assigned to anything, will be registered below
procedure ExceptionNotify(
AExceptionInfo: TEurekaExceptionInfo; var AHandle, ACallNextHandler: Boolean);
begin
// ...
end;
initialization
RegisterEventExceptionNotify(nil, ExceptionNotify);
end.
will work like this:
procedure TForm1.Button1Click(Sender: TObject);
begin
// will be catched by both
// EurekaLogEvents1ExceptionNotify
// and
// ExceptionNotify
raise Exception.Create('Exception From Form');
end;
initialization
finalization
// will be catched by
// ExceptionNotify
// only
raise Exception.Create('Exception From Unit');
end.
See also "Accessing VCL from event handlers" section below.
Accessing global objects from event handlers
EurekaLog event handlers are not synchronized to the main thread. In other words, event handlers will be called by a thread that processes the exception. Therefore, event handlers can be called by any background thread.
Important Note: event handlers can be called from a background thread even in single-thread application. For example, if application raises the Stack Overflow exception, EurekaLog will create a background service thread to handle it.
For the above reasons you should never assume which thread will run your event handlers!
A simplest case is when you don't access any global objects. For example:
procedure ExceptionNotify(
AExceptionInfo: TEurekaExceptionInfo; var AHandle, ACallNextHandler: Boolean);
begin
if AExceptionInfo.ExceptionClass = EMyClass.ClassName then
begin
AHandle := False;
ACallNextHandler := False;
Exit;
end;
end;
initialization
RegisterEventExceptionNotify(nil, ExceptionNotify);
end.
This event handler does not access any globals or external variables, it uses only local information. Therefore, it does not need any additional code.
Another simple case is when you access global static data:
var
GCustomData: Integer;
procedure AddCustomData(const ACustom: Pointer;
AExceptionInfo: TEurekaExceptionInfo;
ALogBuilder: TObject;
ADataFields: TStrings;
var ACallNextHandler: Boolean);
begin
ADataFields.Values['Custom Field 1'] := IntToStr(GCustomData);
end;
initialization
GCustomData := 42;
RegisterEventCustomDataRequest(nil, AddCustomData);
end.
While this code does access the global variable, but this global variable is static, never changes and does not need finalization. Therefore, we can access it safely from any thread without any need for synchronization.
Things become a little bit more complicated if your global variable is dynamic:
var
GCustomData: String;
procedure AddCustomData(const ACustom: Pointer;
AExceptionInfo: TEurekaExceptionInfo;
ALogBuilder: TObject;
ADataFields: TStrings;
var ACallNextHandler: Boolean);
begin
ADataFields.Values['Custom Field 1'] := GCustomData;
end;
initialization
GCustomData := '42';
RegisterEventCustomDataRequest(nil, AddCustomData);
finalization
UnRegisterEventCustomDataRequest(nil, AddCustomData);
end.
While this code does access the global variable, and that global variable requires finalization, but it remains unchanged for the duration of event handler's life cycle. Therefore, the event handler can read the global variable at any time from any thread. See also "Event handlers for leaks" section below.
And the most complex case is accessing global variable that can be changed:
var
GCS: TCriticalSection;
GCustomData: String;
procedure TForm1.FormCreate(Sender: TObject);
begin
GCS.Enter;
try
GCustomData := Caption;
finally
GCS.Leave;
end;
end;
procedure AddCustomData(const ACustom: Pointer;
AExceptionInfo: TEurekaExceptionInfo;
ALogBuilder: TObject;
ADataFields: TStrings;
var ACallNextHandler: Boolean);
begin
GCS.Enter;
try
ADataFields.Values['Custom Field 1'] := GCustomData;
finally
GCS.Leave;
end;
end;
initialization
GCustomData := '42';
GCS := TCriticalSection.Create;
RegisterEventCustomDataRequest(nil, AddCustomData);
finalization
UnRegisterEventCustomDataRequest(nil, AddCustomData);
FreeAndNil(GCS);
end.
This code accesses the global variable, and this global variable can be changed while event handler is registered. It means that you need a proper synchronization to access global variable(s).
Accessing VCL from event handlers
Since event handlers can be called from any thread even in a single-threaded application, the following code is incorrect:
// Assigned to the OnCustomDataRequest event in the TEurekaLogEvents component
procedure TForm1.EurekaLogEvents1CustomDataRequest(
AExceptionInfo: TEurekaExceptionInfo; ALogBuilder: TBaseLogBuilder;
ADataFields: TStrings; var ACallNextHandler: Boolean);
begin
ADataFields.Values['Main Form Caption'] := Caption;
end;
VCL is generally not thread-safe, so you can't access forms from background threads.
If what you are doing is optional - then you can workaround this limitation with a simple check like this:
// Assigned to the OnCustomDataRequest event in the TEurekaLogEvents component
procedure TForm1.EurekaLogEvents1CustomDataRequest(
AExceptionInfo: TEurekaExceptionInfo; ALogBuilder: TBaseLogBuilder;
ADataFields: TStrings; var ACallNextHandler: Boolean);
begin
if GetCurrentThreadID <> MainThreadID then
Exit;
// We are in the main thread
ADataFields.Values['Main Form Caption'] := Caption;
end;
This code is now correct.
However, if you want/need to access VCL from background threads - you do need to perform a proper synchronization. One possible way is using the SendMessage function:
const
WM_FillCustomData = WM_USER + 1;
type
TForm1 = class(TForm)
// ...
protected
procedure FillCustomData(var AMessage: TMessage); message WM_FillCustomData;
end;
// Assigned to the OnCustomDataRequest event in the TEurekaLogEvents component
procedure TForm1.EurekaLogEvents1CustomDataRequest(
AExceptionInfo: TEurekaExceptionInfo; ALogBuilder: TBaseLogBuilder;
ADataFields: TStrings; var ACallNextHandler: Boolean);
begin
SendMessage(Handle, WM_FillCustomData, 0, LPARAM(ADataFields));
end;
procedure TForm1.FillCustomData(var AMessage: TMessage);
var
DataFields: TStrings;
begin
DataFields := TStrings(AMessage.LParam);
// We are inside main thread now
DataFields.Values['Main Form Caption'] := Caption;
end;
This code synchronizes to the main thread by sending a window message to the form.
Important: be extra careful when writing a synchronization code. For example, the following code is incorrect:
procedure TForm1.EurekaLogEvents1CustomDataRequest(
AExceptionInfo: TEurekaExceptionInfo; ALogBuilder: TBaseLogBuilder;
ADataFields: TStrings; var ACallNextHandler: Boolean);
begin
FDataFields := ADataFields;
TThread.Synchronize(nil, FillCustomData);
end;
The problem with this code is that it can be called from two different background threads at the same time like this (read from top to bottom):
Thread 1
FDataFields := ADataFields;
Thread 2
FDataFields := ADataFields;
Thread 1
TThread.Synchronize(nil, FillCustomData);
Event handlers for leaks
Leaks checking happens at a very special moment in your application: everything is already dead (finalized). Be sure to review your handler's code, so it will not access any globals, because these will be already finalized when leaks checking is running. Write code very carefully, don't forget that your whole application is basically already dead. This includes even most basic routines from the SysUtils unit! E.g. even a simple Format may fail, because FormatSettings is already finalized. Definitely no throwing exceptions at this point!
Since leaks checking is such a special place - your typical event handlers will not run, as people usually write handler's code without such strict restrictions. However, you still can register event handler for leaks checking. You just need to (re-)register your event handlers during leaks checks. For example:
uses
EException, // for TEurekaExceptionInfo
EEvents, // for RegisterEventXYZ
ETypes; // for MemLeaksShow
// Your event handlers
// It is just an example
procedure YourHandler1
(const ACustom: Pointer;
AExceptionInfo: TEurekaExceptionInfo;
var AHandle: Boolean;
var ACallNextHandler: Boolean);
begin
// ...
end;
procedure YourHandler2
(const ACustom: Pointer;
AExceptionInfo: TEurekaExceptionInfo;
const AEurekaAction: TEurekaActionType;
const AAdditionalInfo: String;
var AExecute: Boolean;
var ACallNextHandler: Boolean);
begin
// Just an example
if AEurekaAction = atShowingExceptionInfo then
begin
// ...
end;
end;
var
// Default EurekaLog's code to show/process leaks
ShowMemLeaks: TMemLeaksProc;
// Asks EurekaLog to call our event handler for leaks
procedure AssignEventHandlersForLeaks;
begin
// Registers the handlers
RegisterEventExceptionNotify(nil, YourHandler1);
RegisterEventExceptionAction(nil, YourHandler2);
// ...
// Shows leaks
ShowMemLeaks;
end;
finalization
// Asks EurekaLog to call our code when leaks are found
ShowMemLeaks := MemLeaksShow;
MemLeaksShow := AssignEventHandlersForLeaks;
end.
Note: leaks checking is always run from the main thread and all background threads should already be finalized, so no synchronization is (usually) necessary.
Another example: the following code is incorrect:
var
GCustomData: String;
procedure AddCustomData(const ACustom: Pointer;
AExceptionInfo: TEurekaExceptionInfo;
ALogBuilder: TObject;
ADataFields: TStrings;
var ACallNextHandler: Boolean);
begin
ADataFields.Values['Custom Field 1'] := GCustomData;
end;
var
ShowMemLeaks: TMemLeaksProc;
procedure AssignEventHandlersForLeaks;
begin
RegisterEventCustomDataRequest(nil, AddCustomData);
ShowMemLeaks;
end;
initialization
GCustomData := '42';
RegisterEventCustomDataRequest(nil, AddCustomData);
finalization
ShowMemLeaks := MemLeaksShow;
MemLeaksShow := AssignEventHandlersForLeaks;
end.
This code calls AddCustomData for both exceptions and leaks. The issue is that GCustomData global variable will be finalized on app's exit. Therefore, it will be empty during leaks reporting.
Note: you would get this problem only if you exchange data between your normal run-time (exceptions) and shutdown (leaks). If you are using your globals only within normal run-time or only within shutdown - then there will be no such issue.
One possible way to fix this is by using the non-finalized types and manage life time manually:
var
GCustomData: PChar;
// Copies String to PChar
procedure SetString(var AStr: PChar; const AValue: String);
var
StrLen: Integer;
begin
// Remove old value
if AStr <> nil then
begin
{$WARNINGS OFF}
{$IFDEF HAS_MEMORYMANAGER_EX}
UnRegisterExpectedMemoryLeak(AStr);
{$ELSE}
EurekaUnRegisterExpectedMemoryLeak(AStr);
{$ENDIF}
{$WARNINGS ON}
FreeMem(AStr);
AStr := nil;
end;
// Set new value
if AValue <> '' then
begin
StrLen := (Length(AValue) + 1) * SizeOf(Char);
GetMem(Pointer(AStr), StrLen);
Move(Pointer(AValue)^, Pointer(AStr)^, StrLen);
{$WARNINGS OFF}
{$IFDEF HAS_MEMORYMANAGER_EX}
RegisterExpectedMemoryLeak(AStr);
{$ELSE}
EurekaRegisterExpectedMemoryLeak(AStr);
{$ENDIF}
{$WARNINGS ON}
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
SetString(GCustomData, '42');
end;
procedure AddCustomData(const ACustom: Pointer;
AExceptionInfo: TEurekaExceptionInfo;
ALogBuilder: TObject;
ADataFields: TStrings;
var ACallNextHandler: Boolean);
begin
ADataFields.Values['Custom Field 1'] := GCustomData;
end;
var
ShowMemLeaks: TMemLeaksProc;
ShutdownMemLeaks: TMemLeaksProc;
// Ask EurekaLog to call our event handlers for leaks
procedure AssignEventHandlersForLeaks;
begin
RegisterEventCustomDataRequest(nil, AddCustomData);
ShowMemLeaks;
end;
// Removes all globals
procedure ReleaseGlobals;
begin
SetString(GCustomData, '');
// ...
// Add cleanup for any other global variable here
end;
initialization
finalization
// Ask EurekaLog to call our code if leaks will be found
ShowMemLeaks := MemLeaksShow;
MemLeaksShow := AssignEventHandlersForLeaks;
// Ask EurekaLog to clear our global variable(s)
ShutdownMemLeaks := MemLeaksShutdown;
MemLeaksShutdown := ReleaseGlobals;
end.
This code manually allocates memory for custom data during run-time and excludes it from leaks checking. Therefore, this custom data will not be released on shutdown and will not be detected as a leak.
See also:
Send feedback...
|
Build date: 2024-09-30
Last edited: 2023-08-09
|
PRIVACY STATEMENT
The documentation team uses the feedback submitted to improve the EurekaLog documentation.
We do not use your e-mail address for any other purpose.
We will remove your e-mail address from our system after the issue you are reporting has been resolved.
While we are working to resolve this issue, we may send you an e-mail message to request more information about your feedback.
After the issues have been addressed, we may send you an email message to let you know that your feedback has been addressed.
Permanent link to this article: https://www.eurekalog.com/help/eurekalog/understanding_event_handlers.php
|
|