Multicast Events – Part 2

Having covered some of the basic use of multicast events, in this second post I shall start to build the implementation.  In this first iteration we will provide the basics of a multicast event – managing and calling multiple handlers and the ability to enable and disable an event.

The test project used in the previous video demonstration may also now be downloaded for you to experiment with if you wish.

First An Apology

Rather than throwing fully formed code at you and then hoping you can follow me as I attempt to walk you through it, I am instead recreating the process I went through in evolving the implementation, building from the basics and only adding the really clever stuff later.

So apologies if you’d rather just get to grips with the finished implementation – you’ll just have to wait until the series is done.  It won’t actually take that long – one more post in the series I think – but I hope you’ll find the wait worthwhile.

:)

From Little Acorns

As mentioned in part 1, the implementation builds on a base class that provides the foundation for all multicast events.  Rather unimaginatively I called it TMultiCastEvent:

    TMultiCastEvent = class
    private
      fDisableCount: Integer;
      fMethods: TList;
      function get_Count: Integer;
      function get_Enabled: Boolean;
      function get_Method(const aIndex: Integer): TMethod;
      procedure set_Enabled(const aValue: Boolean);
    protected
      procedure Call(const aMethod: TMethod); virtual; abstract;
      procedure Add(const aMethod: TMethod);
      procedure Remove(const aMethod: TMethod);
      property Method[const aIndex: Integer]: TMethod read get_Method;
    public
      constructor Create; virtual;
      destructor Destroy; override;
      procedure DoEvent;
      property Count: Integer read get_Count;
      property Enabled: Boolean read get_Enabled write set_Enabled;
    end;

I shall not go into every tiny detail of the implementation – you can inspect the code yourself in the files accompanying this post – but will concentrate on the (hopefully) more interesting aspects.

For instance, something may strike you as odd about the Add and Remove methods – they have only protected visibility and so will not be visible to us when we create events!  How are we going to add and remove handlers?

We’ll see that this won’t be a problem, and there is a very good reason for doing it this way.

TMethod vs A “Real” Method

The TMethod type, representing the code and data pointers of a method reference, is not particularly usable directly.  I think in part this is because of the special treatment it receives from the compiler and the runtime, although I don’t know this for sure.  Consider for example that Assigned() and NIL work equally well with event handler references and object references, yet event handlers consist of two pointers, where object references consist of only one.  Magic, eh?

A particular problem is that a TMethod record can refer to any type of object method, regardless of that method’s parameters or return type.  Not only does this make working directly with TMethod difficult, it makes it downright dangerous!

So before delving any deeper into TMultiCastEvent, let’s look briefly at the TMultiCastNotify class that is used in the video demo I previously posted, to see how I protect myself from the non-specific nature of TMethod:

    TMultiCastNotify = class(TMultiCastEvent)
    private
      fSender: TObject;
    protected
      property Sender: TObject read fSender;
      procedure Call(const aMethod: TMethod); override;
    public
      constructor Create(const aSender: TObject); reintroduce; virtual;
      procedure Add(const aHandler: TNotifyEvent);
      procedure Remove(const aHandler: TNotifyEvent);
    end;

This class extends TMultiCastEvent for the purpose of creating a multicast version of a TNotifyEvent.

You may have realised by now that you will need to derive a specialised class for each event type for which you need a multicast implementation.  This may sound arduous, but it all depends how many such events you actually need.  In practice I have found that TMultiCastNotify covers almost all of my multicast needs, and you’ll be getting that for free when this series is done!

In TMultiCastNotify we provide public Add and Remove methods with type-safe parameters accepting TNotifyEvent handlers, which simply delegate to the inherited implementation after casting the parameters to the required TMethod type:

  procedure TMultiCastNotify.Add(const aHandler: TNotifyEvent);
  begin
    inherited Add(TMethod(aHandler));
  end;

  procedure TMultiCastNotify.Remove(const aHandler: TNotifyEvent);
  begin
    inherited Remove(TMethod(aHandler));
  end;

NOTE: If the generics support in Tiburon will allow event types to be used, I am happy to admit that I may actually have found a use for them outside of simple collections!  Even I can see the appeal of being able to write:

TMultiCastNotify = class(TMultiCast<TNotifyEvent>)

If anyone from CodeGear is reading this, it would be useful to learn whether this is something that is going to be possible.

;)

For now we will have to make do without generics – which does at least have the advantage of allowing us to use this code in older versions of Delphi!

Managing The Handlers

As mentioned, a TMethod consists of two pointers, so storing these in a TList presents a difficulty.  To get around this, I chose simply to copy each TMethod before adding it to the list.  Although Delphi defines a TMethod type, it doesn’t (as of Delphi 7 anyway) define a corresponding PMethod pointer type, so I declare this for myself allowing me to use New() and Dispose() to manage the copies of TMethod records that I need, and to store pointers to those copies in my fMethods list.

I decided for my implementation that it should not be possible to add the same handler to one event multiple times.  That is, an event may have many handlers, and any one handler might respond to more than one event, but it cannot respond more than once to any one event.

So, before adding a method to the list I first check to ensure it is not already present by comparing the Data and Code pointers to those methods already in the list:

procedure TMultiCastEvent.Add(const aMethod: TMethod);
var
  i: Integer;
  method: PMethod;
begin
  if NOT Assigned(self) then
    EXIT;

  // Ensure that the specified method is not already attached
  for i := 0 to Pred(fMethods.Count) do
  begin
    method := fMethods[i];

    if (aMethod.Code = method.Code) and (aMethod.Data = method.Data) then
      EXIT;
  end;

  // Not already attached - create a new TMethod reference and copy the
  //  details from the specific method, then add to our list of methods
  method := New(PMethod);
  method.Code := aMethod.Code;
  method.Data := aMethod.Data;
  fMethods.Add(method);
end;

Notice that the method checks for a NIL self – this creates a NIL-safe semantic for multicast events. Attempting to add a handler to a NIL multicast event is a null operation, not an error. There is of course a corresponding Remove() method, also NIL-safe, which removes an identified method from the list, remembering that the entry in the list is a copy that has to be disposed once removed:

  procedure TMultiCastEvent.Remove(const aMethod: TMethod);
  var
    i: Integer;
    method: PMethod;
  begin
    if NOT Assigned(self) then
      EXIT;

    for i := 0 to Pred(fMethods.Count) do
    begin
      method := fMethods[i];

      if (aMethod.Code = method.Code) and (aMethod.Data = method.Data) then
      begin
        Dispose(method);
        fMethods.Delete(i);

        // Only one reference to any method can be attached to any one event, so
        //  once we have found and removed the method there is no need to check the
        //  remaining entries.
        BREAK;
      end;
    end;
  end;

These take care of the basics of adding and removing the handler methods for a multicast event, with a bit of type-safety housekeeping from an appropriate derived class.

Enabling Disabling

The implementation of the Enabled property should be fairly obvious from the presence of the fDisableCount property, and the fact that it is an Integer.

Setting Enabled to FALSE increments this count, and setting Enabled to TRUE decrements it.  An event is enabled if the disabled count is zero.

Fairly standard stuff.

Call Me

So much for adding and removing handlers and disabling an event.  The real interest in how event handlers are invoked by an event.  First let’s look at how a normal, unicast event might usually be coded, using an OnClick: TNotifyEvent as an example:

if Assigned(OnClick) then
  OnClick(self);

This should be instantly familiar.  Of course, a multicast event has a list of such handlers but doesn’t need to worry about whether they are Assigned() or not, so to that extent the job is a little easier – the event simply needs to iterate over the list of handlers and call each one in turn.  Easy.

With just one problem.

The list contains (pointers to) TMethod records, with no clues as to what parameters are needed to be passed to those methods.  So, unfortunately, a little work is required in our event specific derived classes.

The base class is able to do one thing for us (under certain circumstances) – it can iterate over the list of handlers and pass each one to that virtual Call() method.  Call() is abstract (the base class simply doesn’t know how to call the methods) – our derived classes must implement Call() and invoke the specified method in the appropriate fashion.

  procedure TMultiCastEvent.DoEvent;
  var
    i: Integer;
  begin
    if NOT Assigned(self) or (NOT Enabled) then
      EXIT;

    for i := 0 to Pred(Count) do
      Call(Method[i]);
  end;

Again, notice that this method has a NIL-safe semantic. Calling DoEvent() using a NIL event reference is a null operation.

In TMultiCastNotify, the implementation of Call() is trivial:

  procedure TMultiCastNotify.Call(const aMethod: TMethod);
  begin
    TNotifyEvent(aMethod)(Sender);
  end;

We cast the method reference back to the appropriate event signature and call it with the appropriate parameters. In this case the only parameter is the Sender, specified when the event was created, so the Call() method itself has all the information it needs to invoke the handlers.

Limitations Of The Base Implementation

As I said, this mechanism is only useful under certain circumstances – it should be apparent that the basic DoEvent() implementation is only of use for events where the parameters passed to the handlers are all specified in the constructor of an event (or set via properties prior to firing) so that no further parameters need be passed to the event when it actually fires.

If the parameters may change from one firing of the event to the next then we cannot use the inherited DoEvent() and an overriden Call() method, we must reintroduce our own DoEvent() and invoke the methods directly.

A hypothetical example of a some event with Sender, X and Y parameters might look something like this:

procedure TMultiCastCoordEvent.DoEvent(const aX, aY: Integer);
var
  i: Integer;
begin
  if NOT Assigned(self) or NOT Enabled then
    EXIT;

  for i := 0 to Pred(Count) do
    TMouseEvent(Methods[i])(Sender, aX, aY);
end;

Notice that Sender is not passed in to DoEvent() – we may assume it was passed via the constructor of the event, just as for TMultiCastNotify – but the X and Y parameters have to be specified at DoEvent() time as they may vary each time the event fires.

It’s a shame that we have to do quite so much work – enforcing the NIL-safe semantics and respecting Enabled indicator as well as iterating over and calling each method. But we only have to do this once for each event signature for which a multicast class need be derived.

I say “have to” but actually there is an alternative.  We could require event parameters to be set-up via properties before firing the event using the inherited DoEvent() implementation:

    procedure TMultiCastCoordEvent.SetParams(const aX, aY: Integer);
    begin
      fX := aX;
      fY := aY;
    end;

    procedure TMultiCastCoordEvent.Call(const aMethod: TMethod); {override}
    begin
      TCoordEvent(aMethod)(Sender, fX, fY);
    end;

  // To fire  - set the params and do the event
  fMouseEvent.SetParams(X, Y);
  fMouseEvent.DoEvent;

If you implement your own multicast events based on TMultiCastEvent you might prefer this approach.

To be perfectly candid, I only just spotted this approach – one of the benefits of explaining something to someone else is that you often see things you didn’t see before!  However, although this alternative approach makes implementing a new multicast event easier (a one time effort) it does come at the expense of slightly more cumbersome usage (a repeated effort).

Your choice.

:)

A Little Effort For Greater Rewards

In any event, whilst an (avoidable) annoyance in many cases, providing a “complete” implementation of DoEvent() for an event class is an opportunity in others, allowing us to introduce different behaviours and capabilities in very specialised event types if required.

For example I have two highly specialised events for very specific purposes in certain projects:

1. A Cancellable Event

The handler signature for this event accepts a var boolean param.  If any handler sets this parameter TRUE, the DoEvent() implementation halts iteration of the handlers, cancelling that firing of the event.

2. Guaranteed First (and Last) Invocation

I also have an event class that supports “Initial” and “Final” handlers – that is, individual unicast handlers that are guaranteed to be invoked before or after – respectively – any handlers in the multicast handler list.

Order of execution for event handlers shouldn’t normally matter.  In fact, a handler should not ordinarily even need to know whether there even are any other event handlers, let alone what order they are being invoked in.  But as I say, I had a very specialised need.

To Recap

The implementation at this stage comprised:

- Support for multicast events

Adding and removing handlers, Enabling, Disabling and firing events

- NIL-safe semantics

Adding/Removing handlers to a NIL event – null operation

Firing a NIL event – null operation

It did however contain a critical flaw.

If a handler were added to an event, and the object that implemented that handler was destroyed without having removed that handler, then the next time the event fired it would call into an invalid handler.  At best an access violation would occur.  At worst some unexpected and undefined behaviour would result.

There is a solution to that problem, and that is what I shall explore in my next post in this series.

In the meantime you may download the demo project and basic multicast event implementation as it stood at this stage in it’s development, to tinker and explore with.

Tags: , , , , ,

2 comments

  1. George Shen’s avatar

    I’ve writen a similar class to implement multicast events for native Win32 Delphi programs, it’s usage is like below list:
    Replace an event with a multicast evnet
    for i := 0 to ControlCount – 1 do
    if (Controls[i] is TButton) then
    MulticastEventManager.Add(@@TButton(Controls[i]).OnClick, TypeInfo(TNotifyEvent));

    or you can use the code:
    MC := TMulticastEvent.Create(@@EHF.OnClick, TypeInfo(TNotifyEvent));

    Then we can add or remove the event handler like
    for i := 0 to ControlCount – 1 do
    if (Controls[i] is TButton) then
    MulticastEventManager[@@TButton(Controls[i]).OnClick].Add(MakePMethod(@TfrmMain.OnClickListener, Self));
    or MC.Add(MakePMethod(@TfrmMain.EHFClick, Self));

    MulticastEventManager[@@TButton(Controls[i]).OnClick] is a TMulticastEvent, this class have these public functions as below:
    function Exists(ACode, AData: Pointer): Boolean; overload;
    function Exists(AMethod: TMethod): Boolean; overload;
    procedure Add(AMethod: PMethod);
    procedure Clear;
    procedure Insert(Index: Integer; AMethod: PMethod);
    procedure Remove(ACode, AData: Pointer); overload;
    procedure Remove(AMethod: TMethod); overload;
    property ContainedInManager: Boolean read FContainedInManager;
    property Handlers: TMethodList read FHandlers;
    property Method: TMethod read FMethod;
    property SavedMethod: TMethod read FSavedMethod;
    property SavedMethodPointer: PMethod read FSavedMethodPointer;

    And some helper functions:
    function MakeMethod(ACode, AData: Pointer): TMethod;
    function MakePMethod(ACode, AData: Pointer): PMethod;
    function MulticastEventManager: TMulticastEventManager;
    function MEMgr: TMulticastEventManager;

    In order to create a TMulticastEvent’s instance, we should provide the Event’s type info [ constructor Create(AMethod: PMethod; ATypeInfo: PTypeInfo); ], because I don’t know how many parameters a function pointer may have. Now TMulticastEvent can support the functions can have 1(like TNotifyEvent) to 11 parameters.

    Here is the code of EventsFirer (without try wrapped)
    // Only support two parameters
    procedure TMulticastEvent.EventsFirer;
    asm
    push ebx
    push esi
    push edi
    push ebp
    mov edi, eax //[esp + $10] // get self
    push ecx
    push edx
    push eax

    // for i := 0 to FHandlers.Count – 1 do
    mov eax, [edi+$04] //FHandlers
    mov ebx, [eax+$08] //FHandlers.Count
    dec ebx
    test ebx, ebx
    jl @endloop
    inc ebx
    xor esi, esi
    // call FHandlers[i];
    @call:
    mov eax, [edi+$04]
    mov edx, esi
    call TListGet
    mov ebp, eax

    push ecx
    push edx
    push eax
    mov eax, TMethod[ebp].Data
    mov edx, [esp + $10]
    mov ecx, [esp + $14]

    call TMethod[ebp].Code

    pop eax
    pop edx
    pop ecx

    inc esi
    // for i := 0 to FHandlers.Count – 1 do
    dec ebx
    jnz @call
    // end;
    @endloop:
    pop eax
    pop edx
    pop ecx

    pop ebp
    pop edi
    pop esi
    pop ebx
    ret
    end;

    Sorry for the long comment.

  2. Jolyon Smith’s avatar

    Thanks for the comment George. I’m not sure if it wasn’t clear that my implementation is also for Win32.

    I’m interested in the use of TypeInfo, although I won’t even begin to pretend to understand the ASM, so I’m not entirely clear how you are using the TypeInfo.

    In my next post in the series I’m going to provide a neat solution to the “dangling handler” problem (when a handler is not removed from an event before the implementing object is destroyed) which may be of interest.

    There are also some nifty time-savers to come. :)

Comments are now closed.