Root > Advanced topics > Understanding event handlers

Understanding event handlers

Previous pageReturn to chapter overviewNext page   

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);

 

FDataFields' value from Thread 1 will be lost (overwritten by Thread 2) and FillCustomData for Thread 1 will operate on the wrong value.

 

 

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: 2023-09-11
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