Active objects

In this chapter I will discuss how to share an automation object among several clients. We will meet the Running Object Table, take a closer look how a client connects its eventsinks to a connectable object, and see how an automation class can implement multiple interfaces.

This story has been inspired by some threads in the Borland newsgroup on automation. Special credit should be given to Greg Lorriman on whose ROTviewer code I have based the ROTviewer presented here.

GetObject and the ROT

Well known automatable applications like Word and Excel register at startup their main  (application) object in the so-called Running Object Table (ROT). Instead of starting a new instance an automation client can connect to the running instance of the application using the GetObject method in VBA:

Dim MyManager As Excel.Application

On Error GoTo CreateNew
   Set MyExcel = GetObject(, "Excel.Application")
   GoTo Created
CreateNew:
   Set MyExcel = New Excel.Application
Created:
   MyExcel.WorkBooks.Open("MyFile")

GetObject will raise an exception if there is no running instance of Excel, I will have to use VBA error handling to handle that. If  GetObject cannot find a running instance I will create a new instance. And when Excel is running one way or the other, my spreadsheet file can be opened.

To work in Delphi with automation objects in the ROT you can use the GetActiveObject function. When I have added the Word typelibrary to my project I can try to connect to Word :

var Unknown : Iunknown;

if (GetActiveObject(Word_TLB.CLASS_WordApplication, nil, Unknown) = MK_E_UNAVAILABLE)  then
   fWordApplication := Word_TLB.CoWordApplication.Create
else
   Unknown.QueryInterface(Word_TLB._Application, fWordApplication);

GetActiveobject is passed the GUID of the desired class, a reserved nil pointer and an out param to hold a resulting interface. If there is no Word application in the ROT the function will return MK_E_UNAVAILABLE and I have to create the Word.Application object myself. If the call to GetActiveObject was successfull I use QueryInterface to obtain the  Word.Application object from the returned interface.

Register your own object in the ROT

Now we have seen how easy it seems to connect to an automation object in the ROT it would be quite nice to register an own object in the ROT so that multiple clients can attach to it. I will take the FileManager demo app again and will try to register that in the ROT. The API functions RegisterActiveObject and RevokeActiveObject can be used to manipulate the ROT. In a first attempt I will Register the FileManager in it's AfterConstruction method.

procedure TFileZapper.AfterConstruction;
   begin
...
   RegisterActiveObject(self as IfileZapper, CLASS_FileZapper, ACTIVEOBJECT_WEAK, cookie);
CoLockObjectExternal(self as IfileZapper, true, true); }

RegisterActiveObject takes as parameters the filezapper object to register, the ID of the Filezapper CoClass, a flag and an out parameter to recieve a cookie. Two values for the flag are available ACTIVEOBJECT_WEAK and ACTIVEOBJECT_STRONG. A strong registration will add an extra reference to the object, preventing an early unload of the object. It is advised to register weak, to prevent premature unloading the manager is locked using the API function CoLockObjectExternal.

The cookie will be  needed to remove the object from the ROT again, which will be done in the BeforeDestruction method.

procedure TFileZapper.BeforeDestruction;
   begin
...
   CoLockObjectExternal(self as IfileZapper, false, true);
   RevokeActiveObject(cookie, nil);
   CoDisconnectObject(self as IfileZapper, 0); }

First the lock is released by a new call to CoLockObjectExternal. After which I can pass the cookie of the object to be removed to RevokeActiveObject. Finally a call to CoDisconnectObject will disconnect all clients from the FileManager object.

An in-process object in the ROT ?

At first sight everything seems to work. I can start a Delphi client app which uses the FileManager after which I will start Word which will connect to this same instance. All seems to work well and both client apps respond to events in the FileManager. I can close Word or start other apps which will connect to the object. But the moment I shut down the first client (the Delphi app) all clients lose their FileManager object.

To understand why we will have to understand what was going on. The Delphi client created the original object. To do so it loaded the FileManager DLL which registered the object in the ROT. Word and all later clients will find this active object in the ROT and start communicating with it. They are talking to the FileManager object in the DLL which was loaded by the Delphi app. And while doing so they are crossing process boundaries. The DLL was an in process server but it is hosted by another process. So now the FileManager has become an out of process server to all except the first client. This shows very nicely how the access to the object is fully transparent to the client. The moment the first client shuts down it will unload the FileManager DLL and the object inside will be gone with it. The other clients will get an "RPC not available" error.

So an object which wants to join the ROT has to reside in an out of process EXE. Clients cannot ask each other to stay alive because they are using that clients object as well.

Hosting the in-process DLL

Instead of rebuilding my server to a standalone EXE I will take another approach. A small proxy executable will create a FileManager object and this proxy will register its interface to this object in the ROT.

fManager:= CoFileZapper.Create;
RegisterActiveObject(fManager, CLASS_FileZapper, ACTIVEOBJECT_WEAK, cookie);
CoLockObjectExternal(fManager, true, true); 

Before closing down the proxy will do the clean up.

CoLockObjectExternal(fManager, false, true);
RevokeActiveObject(cookie, nil);
CoDisconnectObject(fManager, 0); 

And now I am safe. As long as the proxy lives it will house a FileManager object which will be in the ROT. The moment the proxy closes it will remove the object from the ROT.

The Delphi eventsink revisited

Now I have my FileManager in the ROT and I want to share it among several clients. One of these clients will be written in Delphi.  The FileManager does sink events, to sink these to a Delphi client will take some effort. In my first chapter on events I briefly introduced the EventsinkImp tool which generates Delphi implementations of eventsinks. Let's take another look at this handy tool. As a sidestep I will dive deeper into the code it generates. If you get puzzled on the way it is enough to know that we will end up with classes which hide all complexities and are simple to use.

Instead of creating a component I will use the tool to generate a plain tObject. This can be set in the options :

When it's ready I  have nice unit which wraps up all implementation hassles of the sinks :

The unit has three classes. TFileManagerEventsBaseSink is the base class and implements the two basic actions, connecting to and disconnecting from the connectable object.

TFileManagerEventsBaseSink = class (TObject, IUnknown, IDispatch)

The base class implements Idispatch so it can be used to implement the desired dispinterface. When sinking events the invoke method of this dispinterface will be called. This can get quite complex when the event has parameters. The generated base class will sort these parameters out and make a call to the DoInvoke method. This is an abstract method, we will get back to it when we take a look at the implementation of an actual eventsink.

The base class also houses the essential variables of the connection.

FCookie : integer;
FCP : IConnectionPoint;
FSinkIID : TGUID;

A cookie needed to disconnect the sink, the connectionpoint itself and the ID of the implemented sink. Connecting to an object involves passing the eventsink to the object. The connect method will do just that

procedure TFileManagerEventsBaseSink.Connect (const ASource: IUnknown);
var
   pcpc: IConnectionPointContainer;
   begin
   Assert (ASource <> nil);
   Disconnect;
   try
      OleCheck ( ASource.QueryInterface ( IConnectionPointContainer, pcpc ) );
      OleCheck ( pcpc.FindConnectionPoint ( FSinkIID, FCP ) );
      OleCheck ( FCP.Advise ( Self, FCookie ) );
      FSource := ASource;
   except
      raise Exception.Create (Format ('Unable to connect %s.'#13'%s', [ClassName, Exception (ExceptObject).Message] ));
end; { finally }
end;

The code will use QueryInterface to try to obtain the IconnectionPointContainer interface, when successful it will try to find the connectionpoint for the sink class after which it can connect using Advise.

The Disconnect method will stop the sinking of events

procedure TFileManagerEventsBaseSink.Disconnect;
   begin
   if ( FSource = nil ) then Exit;
   try
      OleCheck ( FCP.Unadvise ( FCookie ) );
      FCP := nil;
      FSource := nil;
   except
      pointer (FCP) := nil;
      pointer (FSource) := nil;
   end; { except }
end;

This is straightforward, the connectionpoint's UnAdvise method is called to disconnect.

My FileManager eventsinks are implemented as classes which inherit from the events base class. For all event signatures an appropriate type has been declared

TIFileZapperEventsOnSelectionChangedEvent = procedure (Sender: TObject) of object;
TIFileZapperEventsOnDirectoryChangedEvent = procedure (Sender: TObject; const DirName: WideString) of object;

For each event the class has a property so I can assign a method which will actually handle the event. For each event there is a method which will actually execute this eventhandler :

procedure TFileManagerIFileZapperEvents.DoOnDirectoryChanged(const DirName : WideString);
  begin
  if not Assigned ( OnDirectoryChanged ) then System.Exit;
  OnDirectoryChanged (Self, DirName);
end;

The sinkclass has to implement the abstract DoInvoke method of the baseclass. 

function TFileManagerIFileZapperEvents.DoInvoke (DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; var dps: TDispParams; pDispIds: PDispIdList; VarResult, ExcepInfo, ArgErr: Pointer): HResult;
type
POleVariant = ^OleVariant;

   begin
   Result := DISP_E_MEMBERNOTFOUND;

   //SinkInvoke//
   case DispId of
      1 :
      begin
      DoOnSelectionChanged ();
      Result := S_OK;
      end;

      2 :
      begin
      DoOnDirectoryChanged (dps.rgvarg^ [pDispIds^ [0]].bstrval);
      Result := S_OK;
      end;
   end; { case }

//SinkInvokeEnd//
end;

The DoInvoke method has an enormous load of parameters, most of them are not used here. They make up the whole set which was originally passed to the dispinterface's Invoke method itself. The dispId identifies the event in the sink, it is used in the case selector to fire the appropriate event. When it comes to passing parameters, like in DoOnDirectoryChanged, the parameter is extracted from the parameters record passed to DoInvoke.

All of this would be pretty complicated if you had to code it yourself. Thanks to Binh Ly's tool we have a class for every eventsink supported. The class hides all complexity involved. The connect and a disconnect method of the class will start or stop the sinking of events.

Inspecting the ROT

So far we have only indirectly worked with the ROT using the Get- Revoke- and Register- ActiveObject API functions. The ROT itself is accessible too. I will now create a class which will inspect the ROT and can connect to any IFilezapper instances found there. The sinking of its events will be handled by the classes generated by the eventsink tool.

The ROT is described in the IRunningObjectTable interface, the full declaration can be found in the VCL ActiveX.pas unit. The process of getting objects from the ROT is called binding to an object. Many of the API functions involved require a so-called binding context which is accessed through an IBindCtx interface. This is an interface, declared in the VCL ActiveX.pas unit, to a global binding context.

I will create a new class, tROTmanager, which will wrap up the ROT, a binding context and an interface to the FileManager with its events. The sinking of events can be switched on and off. This leads to these declarations :

ROT : IRunningObjectTable;
BindCtx : IBindCtx;
fManager : IfileZapper;
fZapperEvents : TFileManagerIFileZapperEvents;
fSinkZapperEvents : boolean;
fCollectionEvents : TFileManagerISelectedFileCollectionEvents;
fSinkCollectionEvents : boolean;

The variables will be initialized in the AfterConstruction method.

procedure tROTmanager.AfterConstruction;
   begin
   inherited;
   OleCheck(CreateBindCtx(0, BindCtx));
   OleCheck(GetRunningObjectTable(0, ROT));

   fZapperEvents:= TFileManagerIFileZapperEvents.Create;
   fCollectionEvents:= TFileManagerISelectedFileCollectionEvents.Create;
end;

The API function CreateBindCtx will create a binding context and the GetRunningObject function will hand me an interface to the ROT. The eventsink objects are created from the generated code. They are at hand now to be connected to a IfileZapper interface. When the ROTmanager object is finished I will have to free these eventsinks :

procedure tROTmanager.BeforeDestruction;
   begin
   fZapperEvents.Free;
   fCollectionEvents.Free;
   inherited;
end;

Ok, now I have an interface to the ROT it would be very nice to be able to show what is in the ROT. The ReportTo method will show the ROT's contents in a tListView :

procedure ReportTo(aListView : tListView);

The ROT will be enumerated and for all objects found a listview item is created. The running objects can be obtained by the  IRunningObjectTable.EnumRunning method. This will result in a collection which can be traversed using the standard Next method. All items returned are Imoniker interfaces. Monikers are a world on itself, basically a moniker is some data with the information how to create (or get to) an automation object out of these data. The easiest to grasp are file-monikers. The file-name of Word document combined with the ID of the Word.Document class is enough info to start Word and have it open the file. I will use the moniker returned by the EnumRunning method to get information about the object and to get to the running object itself.

var
   Enum : IEnumMoniker;
   Fetched : integer;
   RunningObj : IMoniker;

if ROT.EnumRunning(Enum) = S_OK then
   begin
   Enum.Next(1, RunningObj, @Fetched);
   while RunningObj <> nil do
     begin

I can enumerate the ROT and will get an Imoniker interface for every object. To get a first idea I will ask every moniker for a display name and will look in the registry to see if I can find some more info on the object the moniker is describing.

reg : tRegistry;

reg:= tRegistry.create;
reg.Access:= KEY_READ;
reg.rootkey:=HKEY_CLASSES_ROOT;

The properties set indicate that I will only read from the registry and will start in classes root, that is where all automation classes have written their info. I will store all info, including the moniker itself in listitems. To provide a place for the moniker a tListItem derived class is declared:

type tMonikerListItem = class(tListItem)
   public
   Moniker : Imoniker;
end;

It adds a placeholder for the Moniker to the standard tlistitem.

Now everything is ready to handle the objects found. The GetDisplayName methods needs the binding context and will return something readable. This will become the caption of the list item. Quite often the Class_ID of the running object will be part of the displayname. The code below will try to extract a classID and look it up in the CLSID subtree of CLASSES_ROOT in the registry. If it finds that, it will read the registry keys for the ProgId and the Inproc- or Local-Server. Note that the actual value of these keys is always the default value under the key, in which case an empty string can be passed to reg.ReadString.

RunningObj.GetDisplayName( BindCtx, nil, Name );

NewItem:= tMonikerListItem.Create( ListView.Items );
ListView1.Items.AddItem( NewItem );
NewItem.Moniker:= RunningObj;
NewItem.Caption:= Name;

sp1:= pos( '{', Name );
sp2:= pos( '}', Name );

try
   if ( sp1 > 0 ) and ( sp2 > 0 ) then
      begin
      ClassHive:= '\CLSID\' + Copy( Name, sp1, sp2-1 );
      if reg.OpenKey( ClassHive, false ) then
         begin
         NewItem.SubItems.Add( reg.ReadString('') );
         reg.CloseKey;
         if reg.OpenKey( ClassHive + '\ProgId', false ) then
            begin
            NewItem.SubItems.Add( reg.ReadString('') );
            reg.CloseKey;
            end;
         if reg.OpenKey( ClassHive + '\InProcServer32', false ) then
            begin
            NewItem.SubItems.Add( reg.ReadString('') );
            reg.CloseKey;
            end;
        if reg.OpenKey( ClassHive + '\LocalServer32', false ) then
            begin
            NewItem.SubItems.Add( reg.ReadString('') );
            reg.CloseKey;
            end;
         end;
      end;
except
   {It 's not a registerd CLSID }
end;

With the demoapplication I have included a ROTviewer which uses this ROTmanager class to show the contents of the ROT. In the FormCreate fSharedManager, a ROTmanager object is, constructed

procedure TForm1.FormCreate(Sender: TObject);
   begin
   fSharedManager:= tROTManager.Create;

A click on a button will now use the ReportTo method to show the contents of the ROT in the form's listview.

procedure TForm1.Button3Click(Sender: TObject);
  begin
  fSharedManager.ReportTo(ListView1);

Now the contents of the ROT can be visualized :

When playing with the viewer some interesting things can be seen :

Getting the FileManager from the ROT

Now I can see the FileManager objects in the ROT, I would like my ROTmanager to connect to them and let the object sink its events into my ROTmanager. In the second tab of the viewer there are some list- and check- boxes as well as some eventhandlers.

procedure TForm1.EventHandler1(sender: tObject);
  begin
  TrackBar1.Position:= TrackBar1.Position + 1;
  end;

procedure TForm1.EventHandler2(sender: tObject; const DirName : Widestring);
  begin
  ListBox1.Items.Add(DirName);
  end;

procedure TForm1.EventHandler3(sender : tObject; const FileName : Widestring);
  begin
  ListBox2.Items.Add(FileName);
  end;

In the formcreate these eventhandlers are bound to the various eventhandler properties of the sinks

fSharedManager.ZapperEvents.OnSelectionChanged:= EventHandler1;
fSharedManager.ZapperEvents.OnDirectoryChanged:= EventHandler2;
fSharedManager.CollectionEvents.OnAdd:= EventHandler3;

To actually recieve the events I will have to connect the sinks. The ROTmanager has a method ConnectSinks which will (dis-)connect the sinks.

procedure tROTmanager.ConnectSinks(sender: tObject);
   begin
   if fManager <> nil then
      begin
      if SinkZapperEvents then
         fZapperEvents.Connect(fManager)
      else
         fZapperEvents.Disconnect;
      if SinkCollectionEvents and (fManager.Selected <> nil) then
         fCollectionEvents.Connect(fManager.Selected)
      else
         fCollectionEvents.DisConnect;
      end;
   end;

The method has to check if it has an interface to the filemanager object. After which it will connect or disconnect the sinks according to the values of the SinkxxxEvents properties. The values of these flags indicating the wish to actually sink events are set by a checkbox :

procedure TForm1.CheckBox1Click(Sender: TObject);
   begin
   fSharedManager.SinkZapperEvents:= (sender as tCheckBox).Checked;
   fSharedManager.ConnectSinks(sender);
end;

After the checkbox'es value has changed the new value is passed to the manager and the manager is asked to update the state of the sinks.

The only thing which is missing is the fManager itself. I will get ths from the ROT. When I click a listitem it is queried to see if it supports the IFileManager interface. If so I will assign the associated object in the ROT to this fManager variable in my ROTmanager object. Each listitem had a moniker to an object in the ROT. I will create a listviewselectitem handler which will use this Moniker to get the object :

procedure tROTmanager.ListViewSelectItem(Sender: TObject; Item: TListItem; Selected: Boolean);

var ppUnk : Iunknown;

   begin
   if not selected then exit;

   try
      OleCheck(ROT.GetObject((Item as tMonikerListItem).Moniker, ppUnk));
      { Try if it supports IFileZapper }
      ppUnk.QueryInterface(IID_IfileZapper, fManager);
      ConnectSinks(sender);
   except
      raise;
   end;

end;

The selected listviewitem will be of type tMonkerListItem, after a Typecast I can get to the moniker. This moniker is passed to IRunningObjectTable.GetObject, which will pass back the running object as an Iunknown. Using QueryInterface I can try to extract its IfileZapper interface. The call to QueryInterface is not checked with the OleCheck function. Not every object in the ROT will be an IfileZapper. If it is something else this will result in fManager being set to nil. There is no need to propagate this in an exception. A call to ConnectSinks will check the state of the sinks.

And all works well. The viewer grabbed the FileManager from the ROT and the filemanager now sinks its events to the viewer.

Introducing the SharedFileManager

The filemanager has been made available by a proxy. This scenario has a small drawback. The moment the proxy starts it will create a FileManager object, which will be running in vain as long as no clients are using it. It would be nice if the object was not created until needed. To get this accomplished I will wrap up the filemanager object in a sharedFileManager object and register this sharedFileManager object in the ROT. The contained filemanager will not be created until it is explicitly asked for.

The sharedfilemanager is also included with the demoapp. The project contains the automationclass SharedZapper. It's typelibarary will use the filemanager typelibrary. In the typelib editor the library has an uses tab. Here I click the right mouse button, choose "show all" and check the filemanager library

Now I can use all types defined in the filemanager typelibrary in this typelibrary. The sharedzapper class wraps up filezapper and its events. To publish this I have to add the interfaces implemented by filezapper in the implements tab of the SharedFileZapper class

 

The IfileZapperEvents interface should be marked as source (of events) and as default.

The fact that the SharedZapper implements all these interface is reflected in its declaration in the unit:

type
TSharedZapper = class(TAutoObject, ISharedZapper, IFileZapper, IconnectionPointContainer)

SharedZapper will delegate the actual implementation of the IFileZapper interfaces to properties using the implements keyword

protected
   property Mananger : IFileZapper read GetManager  implements IFileZapper;
   property CpC : IconnectionPointContainer read GetContainer implements IconnectionPointContainer;

This states that every queryinterface on the object for an IfileZapper interface will be redirected to the Manager property and every queryinterface for an IconnectionPointContainer will be redirected to the CpC property.

Bringing all of this together leads to :

type
   TSharedZapper = class(TAutoObject, ISharedZapper, IFileZapper, IconnectionPointContainer)
   private
      fManager : IFileZapper;
      cookie : integer;
      function GetManager: IFileZapper;
      function GetContainer: IconnectionPointContainer;

   public
      procedure BeforeDestruction; override;

   protected
      property Mananger : IFileZapper read GetManager implements IFileZapper;
      property CpC : IconnectionPointContainer read GetContainer implements IconnectionPointContainer;

end;

The implementation of GetManager will initiate the original FileZapper object and register it in the ROT in the by now well known way.

function TSharedZapper.GetManager: IFileZapper;
   begin
   if fManager = nil then
      begin
      fManager:= CoFileZapper.Create;
      RegisterActiveObject(fManager, CLASS_FileZapper, ACTIVEOBJECT_WEAK, cookie);
      CoLockObjectExternal(fManager, true, true);
      end;
   result:= fManager;
   end;

GetContainer is needed when somebody wants to connect a sink to this object. All GetContainer has to do is redirect the request to the contained fManager object.

function TSharedZapper.GetContainer: IconnectionPointContainer;
   begin
   if fManager <> nil then
      fManager.QueryInterface(IconnectionPointContainer, result);
end;

The implementation of the SharedZapper class is now complete. When the sharedFileManager loads it will create a SharedZapper object and register this in the ROT:

fSharedZapper:= CoSharedZapper.Create;
RegisterActiveObject(fSharedZapper, CLASS_SharedZapper, ACTIVEOBJECT_WEAK, cookie);
CoLockObjectExternal(fSharedZapper, true, true);

When the ROTmanager binds to the object in the ROT it will queryinterface the obtained object for IfileZapper. And that is exactly the moment the property-getter GetManager comes into action and will create the actual FileZapper object. When a client tries to connect a sink to the object it will queryinterface for IConnectionPointContainer. And that is exactly the moment the property-getter GetContainer will come into action.

A few words on MS-Word

As it seems I have now reached all my goals. With the SharedFilManager I can share one instance of the FileManager among multiple clients and  I can wait with the creation of the object itself until the moment it is actually needed. In the chapter on events I have used MS-Word as an events sinking client

For Word to work with the active FileManager object involves a small change in the document open.

Private Sub Document_Open()
   ' Try to connect to running FileManager
On Error GoTo CreateNew
   Set MyManager = GetObject(, "FileManager.FileZapper")
   GoTo Created
CreateNew:
   Set MyManager = New FileManager.FileZapper
Created:

MyManager.FileMask = "*.*"

Word will work with the sharedmanager and Word will recieve the same events as the Delphi client. But Word does have a bug which will show when Word is shut down before the SharedManager is shut down. To cite MS support : "[this is] a known issue with Word's shutdown routine if COM proxies are still Ref counted when Word calls CoUninitialize. In this case COM will attempt to disconnect and free the proxies, but Word has already forced VBA and oleaut32 to unload before CoUnint is called, so the proxy will fault on the disconnect"

What is worse is that Word did not yet revoke its objects from the ROT when crashing. After restarting Word the ROT will be filled with double entries :

And after closing Word there will remain Word entries in the ROT. I will have to restart my machine to get rid of them.

Where are we ?

We have met the global ROT. This is a central place where all automation clients can look for automation objects that are already running. The client can use these objects and can connect its eventsinks to the object. An automation object can implement multiple interfaces. Handing the implementation of an interface to a property can postpone the initialization of the implementing code to the moment the interface is actually requested.

What's next