Safe event hooking for Delphi IDE plugins

 

This is about plugins for the Delphi IDE hooking global events. Sometimes this is necessary because the Open Tools API does not provide access to all necessary aspects of the IDE.

If more than one plugin tries to hook the same event (e.g. Screen.OnActiveFormChange) and even possibly want to remove that hook at a later time they must store the old event handler to call it (chaining the event) and to restore it when the hook is removed. They must also make sure to find their hook again in a possible multi element hook chain.

The following proposes a standard for hooking events that achieves these goals, if all plugins adhere to it.

First, we declares a simple class that can be used by multiple Delhpi IDE plugins to hook any of the global events in a manner that they can remove the hook without wreaking havoc for the other plugins or the IDE itself. It works only for TNotifyEvents but a similar class could (and should) be written for other event types.

unit SomeUnitNameThatMustBeUniqueForTheIDE;
// We could probably put this unit into a package and include this
// package in all IDE plugins, but it's easier to just have a copy
// of this unit in all the plugins and give them a unique name.
// e.g. GExperts could call it GX_NotifyEventHook

interface

uses
  SysUtils,
  Classes;

type
  ///<summary>
  /// This class is meant to be used as the destination of a TNotifyEvent.
  /// It is kept very simple on purpose.
  /// Use it as is. Do *not* add to it or derive from it.
  /// Do *not* rename the class.
  /// See the example procedures below on how to use it. </summary>
  TNotifyEventHook = class
  public
    ///<summary>
    /// Store the original event here. </summary>
    OrigEvent: TMethod;
    ///<summary>
    /// Store the event you want to call here. </summary>
    HookEvent: TMethod;
    ///<summary>
    /// Assign this method to the Event you want to hook.
    /// It first calls HookEvent and afterwards OrigEvent. </summary>
    procedure HandleEvent(Sender: TObject);
    constructor Create(_OrigEvent: TMethod; _HookEvent: TMethod);
  end;

implementation

{ TNotifyEventHook }

constructor TNotifyEventHook.Create(_OrigEvent, _HookEvent: TMethod);
begin
  inherited Create;
  OrigEvent := _OrigEvent;
  HookEvent := _HookEvent;
end;

procedure TNotifyEventHook.HandleEvent(Sender: TObject);
begin
  if Assigned(HookEvent.Data) and Assigned(HookEvent.Code) then
    TNotifyEvent(HookEvent)(Sender);
  if Assigned(OrigEvent.Data) and Assigned(OrigEvent.Code) then
    TNotifyEvent(OrigEvent)(Sender);
end;

end.

(If you want to copy the code to the clipboard move your mouse to the upper right corner of the display and click on the <> symbol there. It will open the code in a separate window without line numbers and easy to select and copy.)

As the comment already says: We could probably put this unit into a package and include this package in all IDE plugins, but it’s easier to just have a copy of this unit in all the plugins and give them a unique name. e.g. GExperts could call it GX_NotifyEventHook

The class is kept very simple on purpose. Use it as is. Do *not* add to it or derive from it. Do *not* rename the class. The unhooking code relies on all hooks to have the same structure and the same class name.

TNotifyEvent is by far the most used event type, but a similar class can easily be written for a different event type. Here is one for TCloseEvent (Used by TForm.OnClose). I’ll leave out any comments for readability:

interface

type
  TCloseEventHook = class
  public
    OrigEvent: TMethod;
    HookEvent: TMethod;
    procedure HandleEvent(Sender: TObject; var Action: TCloseAction);
    constructor Create(_OrigEvent: TMethod; _HookEvent: TMethod);
  end;

implementation

{ TCloseEventHook }

constructor TCloseEventHook.Create(_OrigEvent, _HookEvent: TMethod);
begin
  inherited Create;
  OrigEvent := _OrigEvent;
  HookEvent := _HookEvent;
end;

procedure TCloseEventHook.HandleEvent(Sender: TObject; var Action: TCloseAction);
begin
  if Assigned(HookEvent.Data) and Assigned(HookEvent.Code) then
    TCloseEvent(HookEvent)(Sender, Action);
  if Assigned(OrigEvent.Data) and Assigned(OrigEvent.Code) then
    TCloseEvent(OrigEvent)(Sender, Action);
end;

Unfortunately, since this event has got different parameters and the HandleEvent method must handle these parameters and pass them on, it is not possible to have only one class for all event types (OK, it probably is, if you resort to assembler).

Once such an event hook class is declared, here is how to use it. This example hooks the Screen.OnActiveControlChange event (which is a TNotifyEvent):

// Assume you want to hook Screen.OnActiveControlChange
// (which is a TNotifyEvent) with the method
// HandleActiveFormChange of your already existing object MyObject.
// So you call HookScreenActiveFormChange like this:
// Hook := HookScreenActiveFormChange(MyObject.HandleActiveControlChange);

function HookScreenActiveControlChange(_HookEvent: TNotifyEvent): TNotifyEventHook;
begin
  Result := TNotifyEventHook.Create(TMethod(Screen.OnActiveControlChange), TMethod(_HookEvent));
  Screen.OnActiveControlChange := Result.HandleEvent;
end;

You call HookScreenActiveFormChange like this:

MyHook := HookScreenActiveFormChange(MyObject.HandleActiveControlChange);

That’s it, now your method will be called as the first in the event chain. If there already was a different method assigned to the event, it will be called afterwards. Other plugins can do the same, so your method will move back in the chain but it will still be called. The hook object returned by the function should be preserved to be used for unhooking.

It is also possible to add a new hook somewhere else in the event chain e.g. at the end, but I’m for now not going into that.

Now for the difficult part: You are done and want to unhook your method from the event chain:

procedure UnhookScreenActiveControlChange(_Hook: TNotifyEventHook);
var
  Ptr: TMethod;
  PrevHook: TNotifyEventHook;
begin
  // Typecast the event to TMethod so we can access the
  // Code and Data parts individually
  Ptr := TMethod(Screen.OnActiveControlChange);

  if not Assigned(Ptr.Data) and not Assigned(Ptr.Code) then begin
    // Somebody has assigned NIL to the event.
    // It's probably safe to assume that there will be no reference
    // to our hook left, so we just free the object and be done.
    _Hook.Free;
    Exit;
  end;

  if Ptr.Data = _Hook then begin
    // We are lucky, nobody has tampered with the event,
    // we can just assign the original event,
    // free the hook object and be done with it.
    Screen.OnActiveControlChange := TNotifyEvent(_Hook.OrigEvent);
    _Hook.Free;
    Exit;
  end;

  if TObject(Ptr.Data).ClassNameIs(_Hook.Classname) then begin
    // Somebody else, who knows about this standard, has hooked the event.
    // (Remember: Do not change the class name or the class
    // structure. Otherwise this check will fail!)
    // Let's check whether we can find our own hook in the chain.
    PrevHook := Ptr.Data;
    Ptr := PrevHook.OrigEvent;
    while Assigned(Ptr.Data) and TObject(Ptr.Data).ClassNameIs(_Hook.Classname) do begin
      if Ptr.Data = _Hook then begin
        // We found our own hook. Remove it from the chain and be done.
        PrevHook.OrigEvent := _Hook.OrigEvent;
        _Hook.Free;
        Exit;
      end;
      // check the next hook in the chain
      PrevHook := Ptr.Data;
      Ptr := TNotifyEventHook(Ptr.Data).OrigEvent;
    end;
  end;

  // If we get here, somebody, who does not adhere to this standard,
  // has changed the event. The best thing we can do, is assign NIL
  // to the HookEvent so it no longer gets called.
  // We cannot free the hook because somebody might still have a
  // reference to _Hook.HandleEvent. So there will be a small
  // memory leak.
  _Hook.HookEvent.Code := nil;
  _Hook.HookEvent.Data := nil;
end;

You call it like this:

UnhookScreenActiveControlChange(MyHook);

It will try to safely unhook the event and free the hook if possible. Since there is a chance that another plugin or the IDE has tampered with the event, it is not necessarily possible to free the hook because there might still be references to it. In that case the HookEvent property will be set to nil so your event handler will no longer be called but the hook object cannot be freed creating a small memory leak. Since the hook object is very small, this won’t matter much (the IDE already has memory leaks aplenty).

So, if all plugins adhere to this standard, they can safely hook and unhook any global event handlers. This code should work for all 32 bit Delphi versions, probably even back to Delphi 2.

This code is not thread safe. I can’t see a way to make it thread safe because even if there was a named mutex and all plugins used it, the Delphi IDE knows nothing about it and might still fire events while some plugin is hooking or unhooking an event handler. Hm, thinking about it: There are some Windows functions that allow changing 64 bit values as an atomic operation, so maybe we could use that to make the code thread safe?

%d bloggers like this: