Date: September 1, 2007
Author: Dennis Landi


A Delphi Run-time Package Plug-in Solution

The Problem:


After over a decade of using Borland’s Delphi Run-time Packages as “add-ons or plug-ins” for my applications and gritting my teeth at the sheer ornery-ness of these critters, it finally occurred to me last week (while loudly complaining on the Borland/Codegear newsgroups) that I could do something about it!  Doh!

My main complaint with the Delphi Run-time Package system is that its design is incomplete.  What I find lacking is a proper system-wide (application-wide) notification when a class is registered in an application, via a run-time package loaded by calling the procedure LoadPackage().  

I was trying to figure out how to surface an OnRegisterClass event handler that I could consume in my application that would fire any time the RegisterClass() was called from a packaged unit loaded via LoadPackage().  This functionality is essential to achieve a basic plug-in design, which is of course what a run-time package is: A Plug-in...


Solution A) An IDEAL (when pigs fly) WORLD Solution:


For future versions of Delphi  the solution is trivial:

1) Add a new event-handler to TApplication:

  TClassRegisteredEvent = procedure(AClass: TPersistentClass) of object;

  TApplication = class(TComponent)
    private
     FOnClassRegistered:TClassRegisteredEvent;
    public
    property OnClassRegistered:TClassRegisteredEvent read FOnClassRegistered write FOnClassRegistered;
  end;

2) create a wrapper for RegisterClass that calls an TApplication.OnRegisterClass.

procedure RegisterClassEx(AClass: TPersistentClass);
begin
    classes.registerClass(AClass);
    if assigned(Application.OnClassRegistered) then
      Application.OnClassRegistered(AClass);
end;

That's it CodeGear, you're done.  We would have basic plug-in functionality with just normal run-of-the-mill application code…


Solution B) Windows Messaging:


However, that doesn't help us out here in the trenches using current and past versions of Delphi on a daily basis.  So below is a solution that should work for just about any version of Delphi.   We’ll use windows messaging via the wm_CopyData message to communicate with the mainform of TApplication.  By writing a wm_CopyData message handler in your application, you now have a mechanism to notify the application when a class is registered that has been loaded in a run-time package. 

Because we know precisely what the Application.Mainform's windows handle is from within a run-time package, we can send the message straight to the intended recipient.

The entire source for a working example is provide below and here is a zip file for the same example. This is just a copy of what I posted on the newsgroups last week.


//*********The Package Code ****************************

unit uPkgTest;
//---------A Unit In a Run-time Pkg---------------------
interface

uses   Windows, Messages, SysUtils, Variants, Classes, Forms,Dialogs, StdCtrls, ExtCtrls, Controls;

  type
  TTest01 = class(TPersistent)
  private
    { Private declarations }
  public
    { Public declarations }
  end;

implementation

procedure SendData(copyDataStruct: TCopyDataStruct) ;
var
   receiverHandle : THandle;
   recieverClassName, recieverName:string;
   cbInt : integer;//callback value
begin
   recieverName := application.mainform.Name;
   recieverClassName:= application.mainform.ClassName;
   showmessage('LoadPackage - '+#13#10+'Get the Application''s MainForm Name: '+#13#10+application.mainform.ClassName);
receiverHandle:= FindWindow(PChar(recieverClassName),PChar(recieverName)) ;
   if receiverHandle = 0 then
   begin
     ShowMessage('CopyData Receiver NOT found!') ;
     Exit;
   end
   else showmessage('MainForm''s Handle is: '+intToStr(receiverHandle));

  cbInt := SendMessage(receiverHandle, WM_COPYDATA, 0, Integer(@copyDataStruct));

  if cbInt > 0 then ShowMessage(Format('Receiver has sent back this value: %d !',[cbInt]));
end;

procedure SendString(s:string) ;
var
   copyDataStruct : TCopyDataStruct;
begin
   copyDataStruct.dwData := 0; //use it to identify the message contents
   copyDataStruct.cbData := 1 + Length(s) ;
   copyDataStruct.lpData := PChar(s) ;
   SendData(copyDataStruct) ;
end;

procedure registerClassEx(AClass:TPersistentClass);
begin
  registerClass(AClass);
  sendString(AClass.ClassName);
  //application should not be NIL... But Application.Mainform will be nil until it initializes...
  if application = nil then showmessage('Application is nil');
end;

initialization
  registerClassEx(TTest01);
finalization
  unregisterClass(TTest01);

end.

//**********And below is code to drop into any MainForm ********************
//**** NOTE: Since we can't get an event handler in TApplication, **********
//***** TApplication.MainForm  is our next best generic option...
**********

unit unit01;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes,
Graphics, Controls, Forms,  Dialogs, StdCtrls;

type
  TForm6 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
   procedure WMCopyData(var Msg : TWMCopyData) ; message WM_COPYDATA;
    procedure HandleCopyDataString(copyDataStruct: PCopyDataStruct);
  public
    { Public declarations }
  end;

var
  Form6: TForm6;

implementation

{$R *.dfm}

procedure TForm6.Button1Click(Sender: TObject);
begin
  if FileExists('Pkg01.bpl') then
    begin
      loadPackage('Pkg01.bpl') ;
    end
    else beep;
end;

procedure TForm6.HandleCopyDataString(copyDataStruct: PCopyDataStruct);
var
  s : string;
begin
  s := PChar(copyDataStruct.lpData);
  showmessage('ClassName: '+s+' Recieved!');
end;

procedure TForm6.WMCopyData(var Msg: TWMCopyData);
begin
  HandleCopyDataString(Msg.CopyDataStruct);
  //Send something back
  msg.Result := 1111111;
end;
end.

This solution above provides a rough and ready way to add a Run-time Package Class-registered notification system directly into your application code.  The main problem I have with this solution is that the message-sender to message-receiver granularity is too coarse, forcing us to put filtering code in our wm_CopyData message handler to react to each class registration.

 


Solution C) TPlugInMgr Component (distributions: Delphi7 and Delphi2006)

Note: You will have to modify the search and output directory paths in each project file to make sure they are pointing to the correct path names on your local installation.

We’ve solved the problem of needing a tighter sender to receiver coupling by moving the wm_CopyData message handler into a TWinControl descendant that has a windows handle and can receive windows messages.  Now we can couple class registration in each run-time package with a specific registration event handler at run-time.  We can make this coupling easier by giving TPlugInMgr a component editor that a developer can use to generate unit code that is “tuned” to a particular TPlugInMgr instance.

NOTE: Because of the windows messaging event model at the core of this component, the packages are loaded asynchronously. To see this in the demo set the property LoadPkgOnStart to "True" for both components; and you will then see that both components will load their packages virtually simultaneously. To "daisy-chain" the loading of packages see the included demo. You will notice that the first package utilizes the ILastClassInPkg interface for the last class registered in the package. The component can use this as a flag to determine that this class is the last in the package to load and sets the isLastClassInPkg parameter in the OnClassRegister event-handler to True. This allows the developer to call LoadPackages() for the next TPlugInMgr in the load sequence. A developer can simply flag the last class registered in his package by addind the ILastClassInPkg interface to his list of supported interfaces for the class. Nothing more needs to be done.

 

 

Updates to TPlugInMgr

Changes:

//-------------------------------------------
version 1.0002 September 6, 2007

- Added the IMetaInfo interface class in PkgPluginMgrIntf.pas


- Added a MetaInfo Class Method facility to surface developer provided
meta-info for a registered class in TPlugInMgr's OnRegisteredClass
event Handler. Look in the uPkg01Test packaged unit to see how this
is done. Also look in the HandleClassRegisterEvent() procedure to see how
this info is surfaced in the event-handler.

//-------------------------------------------
version 1.0001 September 3, 2007

- To avoid confusion when multiple instances of
the same app are running:

      - Added LoadingPkg property to guard against
         multiple applicaton instance confusion.

      - Added a FindClass() check before firing
         the OnClassRegistered event handler.

      - The SendMsg function in the package unit
         now sends the application windows handle in
         the "msg.From" field so that it can be matched
         against the recieving applicaton's handle.

- Added ILastClassRegistered interface class to a new
unit, PkgPluginMgrIntf.pas, which resides in the same
directory as this component. See the demo to see how to
use this interface to "daisy-chain" package loading.

- fixed a little glitch in the template constant generation.
uPkgTemplate constant string value now wraps at 70 chars, preformatted
a little better.
//------------------------------------------

 


Source Code documentation for TPlugInMgr component

PluginMgr01 unit

Classes
TWaitForMainFormThread
TPlugInMgrCompEditor
TPlugInMgr
Types
TPackageLoadedEvent
TClassRegisteredEvent
Functions and Procedures
Register
GetTemplate
URLEncode
URLDecode
RegisterGlobalPkg
UnregisterGlobalPkg

UML Diagram of TPlugInMgr

TPlugInMgr
Unit
PluginMgr01
Description
Notice that TPlugInMgr descends from TCustomStaticText.  This is because this component needs a windows handle and the ability to recieve windows messages.  By subclassing this light-weight  TwinControl we get the immediate bonus of being able to see the identity of the component painted on its surface at design time. Additionally, we have a future opportunity to create a TPlugInMgr  descendant that can display its status directly on the surface  of its control.

Fields
FAttr
FFileMask
FileList
FLoadPkgOnStart
FOnClassRegistered
FOnPackageLoaded
FPath
FPlugInDir
FStowAway
LoadedPkgs
WaitForMainFormThread


Methods
AbsolutePath
Create
CreatePackageUnit
Destroy
FileSearch
HandleClassRegisterEvent
Loaded
LoadPackages
LoadPkg
RegisterPkg
SearchForFiles
SetName
SetPath
UnloadPackages
UnloadPkg
WMCopyData

Properties
AbsolutePlugInPath
Caption
FileAttr
FileMask
LoadPkgOnStart
PlugInDir
StowAway

Events
OnClassRegistered
OnPackageLoaded

 

Element Descriptions Follow:

AbsolutePlugInPath property
Applies to
TPlugInMgr
Declaration
property AbsolutePlugInPath: string read FPath write SetPath; public
Description
Property AbsolutePlugInPath is read / write, at run time only.


Caption property
Applies to
TPlugInMgr
Declaration
property Caption; public
Description
Property Caption.
Since component descends from the winControl,  TCustomStaticText, the Caption Property needs  to moved from Protected to Public so that  windows messages can target this control,  which depends on access to the "window title" of the control.


FileAttr property
Applies to
TPlugInMgr
Declaration
property FileAttr: TFileAttr read FAttr write FAttr; public
Description
Property FileAttr is read / write, at run time only.


FileMask property
Applies to
TPlugInMgr
Declaration
property FileMask: string read FFileMask write FFileMask; published
Description
Property FileMask is read / write at run time and design time.
The default is "BPL". And controls the FileSearch() behavior.


LoadPkgOnStart property
Applies to
TPlugInMgr
Declaration
property LoadPkgOnStart: Boolean read FLoadPkgOnStart write FLoadPkgOnStart; published
Description
Property LoadPkgOnStart is read / write at run time and design time. If LoadPkgOnStart=True then the thread WaitForMainThread is activated which waits for Application.MainForm to become initialized, so that it can automatically call loadPackage().  The package(s) to be loaded by the PlugInMgr depend on access to Application. Mainform.


PlugInDir property
Applies to
TPlugInMgr
Declaration
property PlugInDir: string read FPlugInDir write FPlugInDir; published
Description
Property PlugInDir is read / write at run time and design time.
Automatically set by the component to mirror  the component name to the component's specific  plug-in directory.  Modifying this automatic value is not advised.


StowAway property
Applies to
TPlugInMgr
Declaration
property StowAway: Boolean read FStowAway write FStowAway; published
Description
Property StowAway is read / write at run time and design time.
When StowAway=True the component will become  hidden at run-time.  We cannot simply set its Visible property to False, because this also disables the ability for this control to recieve messages.


OnClassRegistered event
Applies to
TPlugInMgr
Declaration
property OnClassRegistered: TClassRegisteredEvent read FOnClassRegistered write FOnClassRegistered; published
Description
Event OnClassRegistered is dispatched.
EventHandler that fires when a packaged "tuned" to the PlugInMgr registers a Class via the custom procedure RegisterClassEx (which wraps  sysUtils.registerClass).


OnPackageLoaded event
Applies to
TPlugInMgr
Declaration
property OnPackageLoaded: TPackageLoadedEvent read FOnPackageLoaded write FOnPackageLoaded; published
Description
Event OnPackageLoaded is dispatched.
Event that fires when a Package is Loaded


AbsolutePath method
Applies to
TPlugInMgr
Declaration
procedure AbsolutePath; private
Description
procedure AbsolutePath.
This procedure builds the Full File Path to the Directory to which this  PlugInMgr component is mapped.  It will search for BPLs to load in this directory only.


Create method
Applies to
TPlugInMgr
Declaration
constructor Create(AOwner: TComponent); override; public
Description
Constructor Create overrides the inherited Create. First inherited Create is called, then the internal data structure is initialized


CreatePackageUnit method
Applies to
TPlugInMgr
Declaration
procedure CreatePackageUnit; public
Description
procedure CreatePackageUnit.
This procedure will take the string constant  uPkgTemplate and generate a new packaged unit  that will be saved to a user designated file and  path. The new unit will be specifically reference the component that generated, and will ease the  developer's task in building the PlugInMgr package.


Destroy method
Applies to
TPlugInMgr
Declaration
destructor Destroy; override; public
Description
Destructor Destroy overrides the inherited Destroy. First all owned fields are freed, finally inherited Destroy is called.


FileSearch method
Applies to
TPlugInMgr
Declaration
procedure FileSearch(const inPath : string); private
Description
procedure FileSearch. inPath.
Uses the parameter inPath to search the directory defined by AbsolutePath to fill the stringlist FileList with any BPLs that match the FileMask criteria.  This procedure is called by the  function SearchForFiles.


HandleClassRegisterEvent method
Applies to
TPlugInMgr
Declaration
procedure HandleClassRegisterEvent(PlugInID:integer; CopyDataStruct: PCopyDataStruct); private
Description
procedure HandleClassRegisterEvent. PlugInID, CopyDataStruct.
This procedure exposes the eventHandler, OnClassRegistered,  passing in the PlugInID and the registered ClassName to the  event-handler.


Loaded method
Applies to
TPlugInMgr
Declaration
procedure Loaded; override; protected
Description
procedure Loaded overrides inherited Loaded.
Executes the "Stow-away" functionality, which hides the PlugInMgr Component with stowAway=True. If LoadPkgOnStart=True then the WaitForMainThread is launched here as well.

 

LoadPackages method
Applies to
TPlugInMgr
Declaration
procedure LoadPackages; public
Description
procedure LoadPackages.
This passes AbsolutePlugInPath to FileSearch() and then calls LoadPkg() for each file found.


LoadPkg method
Applies to
TPlugInMgr
Declaration
procedure LoadPkg(PkgNameAndPath:String); public
Description
procedure LoadPkg. PkgNameAndPath.
This procedure loads a package identified by the  PkgNameAndPath parameter, registers the class  name locally via RegisterPkg() and then calls the OnPackageLoaded event-handler.


RegisterPkg method
Applies to
TPlugInMgr
Declaration
procedure RegisterPkg(PkgName: string; H: HModule); private
Description
procedure RegisterPkg. PkgName, H.
This procedure registers, within the component, class names  of classes newly registered via sysUtils.LoadPackage();   This procedure also registers the class name globally across all component instances.


SearchForFiles method
Applies to
TPlugInMgr
Declaration
function SearchForFiles: TStringList; public
Description
function SearchForFiles. Returns:
this function calls FileSearch() and populates  the stringlist field FileList.


SetName method
Applies to
TPlugInMgr
Declaration
procedure SetName(const Value: TComponentName); override; protected
Description
procedure SetName overrides inherited SetName. Value.
We update the Caption property here to match the Component's name. We also build the FPlugInDir field here,  which is in turn consumed by AbsolutePath().


SetPath method
Applies to
TPlugInMgr
Declaration
procedure SetPath(Value: string); private
Description
SetPath is the write access method of the AbsolutePlugInPath property.


UnloadPackages method
Applies to
TPlugInMgr
Declaration
procedure UnloadPackages; public
Description
procedure UnloadPackages.
This procedure unloads only the packages that are being managed by this particular PlugInMgr component, which it loaded via LoadPkg(). It calls UnloadPkg() for each BPL handle it is  holding.


UnloadPkg method
Applies to
TPlugInMgr
Declaration
procedure UnloadPkg(PkgName: String); public
Description
procedure UnloadPkg. PkgName.
Unloads a package and unregisters it from the local and global package list within the unit.


WMCopyData method
Applies to
TPlugInMgr
Declaration
procedure WMCopyData(var Msg : TWMCopyData); message WM_COPYDATA; public
Description
procedure WMCopyData handles the WM_COPYDATA message. Msg.
Windows message proc that intercepts wm_copydata and calls HandleClassRegisterEvent.  This windows message was triggered by loading a package that was specifically "tuned" to the current PlugInMgr receiving the message.



TWaitForMainThread Image

TWaitForMainFormThread
Unit
PluginMgr01
Description
This thread class is contained as a field by each instance of TPlugInMgr.  When TPlugInMgr.LoadPkgOnStart=True this thread is  launched so that it can wait for Application.MainForm to initialize so that it can then call LoadPackages() for its host component. Since this class is so trivial, I won't include further documentation of it. Look at the source for the few lines of code in this class.



TPlugInMgrCompEditor

TPlugInMgrCompEditor
Unit
PluginMgr01
Description
This is the component editor for TPlugInMgr.  It exposes two  functions via its speed-menu (right-click on the component at design time in the IDE), 1) CreatePackageUnit( ) and 2) StorePackageUnitTemplate( ).


Methods
CreatePackageUnit
StorePackageUnitTemplate

CreatePackageUnit method
Applies to
TPlugInMgrCompEditor
Declaration
procedure CreatePackageUnit; public
Description
Calls CreatePackageUnit() in TPlugInMgr.

A template of a Packaged Unit that is tuned to TPlugInMgr is stored in the component's source file.  Choose this  option to create a new package unit with all the plumbing necessary to  communicate with the component at runtime when it is loaded via  sysUtils.LoadPackage().

StorePackageUnitTemplate method
Applies to
TPlugInMgrCompEditor
Declaration
procedure StorePackageUnitTemplate; public
Description

Use this option to modify the  packaged unit template which is stored in the const uPkgTemplate.   The unit uPkgTemplate.pas is stored in the directory ../PkgUnitTemplate.   Modify this unit to your taste and then use this Component Editor option to store your changes in the uPkgTemplate constant in the source unit. After this procedure executes, the new template text is on the clipboard ready to be copied into your source file..

<hr width="500">