Title: Creating Well-Designed Plug-In's
Question: How to merge all benefits of dll's and bpl's to create plugin's?
Answer:
History:
2005/05/09: Added MultiPlugIn Demo Download
2004/11/18: Added Demo Download
Introduction
==============
Some months ago, I searched for (simple) delphi solutions about how to
create and use plugin's in my apps. Well, I found two different approaches:
1) Using native Windows-DLL's, and
2) Using Borland BPL's
Both solutions have advantages and disadvantages:
Native Windows-DLL's:
+ can be used and developed in other programing languages (except they
used native delphi stuff)
+ no need of runtime packages (see below)
+ common usage
- possible large filesize (dependant of units used)
- no well-designed interface(s) - back to the roots!
Borland BPL's:
+ you can use well-designed interface(s)
+ smaller filesize (use of common libraries)
+ easy debugging
- your app requires runtime packages (e.g. VCL70.bpl, ...)
- plugin's can only be created & used by delphi
So which method to use? I hate the old 'back-to-the-roots' method of creating
proxy methods (or type definitions) to use an interface using native dll's
but also be deterred of linking runtime packages to my app using bpl's.
What I want?
==============
I want to merge the advantages of both methods and as less disadvantages as
possible:
+ creating a dll (instead of a bpl)
+ using a well-designed interface to access the methods
+ no need of runtime packages (in the main app)
+ easy debugging
The following solution attemp to accord this model with only one disadvantage:
- you can't use any other programing language except delphi to create and
use the plugin's.
- (the compiled 'dll' could be larger than a bpl using other common libraries,
like 'VCL70.bpl', but if you sum this additional units, the resulting
filesize is much larger.)
This isn't a big problem if you onyl deal with delphi (and we're on delphi3000
here 8-)
Solution
==========
The following solution can be extended by yourself to fit your needs. It's
part of one of my applications and works fine.
Most plugin's have a common interface which should be implemeneted by any
descendants like the author, name, version and description, so the following
code snippet could be used as our base:
unit PlugInIntf;
interface
uses
Classes;
type
{ TPlugInClass }
TPlugInClass = class(TComponent);
{ TPlugInProc }
TPlugInProc = function(AOwner: TComponent;
ForceCreate: Boolean = False): TMyPlugInClass; stdcall;
{ ICustomPlugIn }
ICustomPlugIn = interface
['{DBF39F5D-C567-4E6D-987C-B228933399E4}']
function GetAuthor: string;
function GetName: string;
function GetDescription: string;
function GetVersion: string;
end;
implementation
end.
You don't need this base interface if your plugin's should be all individual
but in this case, you need at least one 'parent'-interface to derive from.
ICustomPlugIn = interface
['{DBF39F5D-C567-4E6D-987C-B228933399E4}']
end;
To manage all our plugin's, we'll create a so-called plugin-manager which
allows us to load & unload them. This manager needs an entrypoint which
would be called if the plugin is being loaded (see above):
- TPlugInClass
- TPlugInProc
The 'ForceCreate' property can be used to differ the plugin to create it's
datamodules or only to return it's common properties (name, version, ...).
So far so good, how does a 'real' plugin could look like?
Sample PlugIn Interface
=========================
The interface of our plugin will be derived from 'ICustomPlugIn' and
implements only two additional methods:
unit SamplePlugInIntf;
interface
uses
PlugInIntf, Classes;
const
CS_NAME = 'SamplePlugIn';
CS_VERSION = '1.0.0';
type
{ ISamplePlugIn }
ISamplePlugIn = interface(ICustomPlugIn)
['{BD6F283C-147B-4F6F-814C-4FFBB0548AE9}']
function Sum(a, b: Integer): Integer;
procedure DisplayMessage(const MyText: string);
end;
implementation
end.
SamplePlugIn Implementation
=============================
The implementation header of our plugin is also quite simple:
unit SamplePlugInImpl;
interface
uses
PlugInIntf, SamplePlugInIntf, Classes;
type
{ TSamplePlugIn }
TSamplePlugIn = class(TPlugInClass, ICustomPlugIn, ISamplePlugIn)
private
{ Private-Deklarationen }
public
{ Public-Deklarationen }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function GetAuthor: string;
function GetName: string;
function GetDescription: string;
function Sum(a, b: Integer): Integer;
procedure DisplayMessage(const MyText: string);
end;
As you may have noticed, the plugin has been derived from 'ICustomPlugIn'
as well as from our 'ISamplePlugIn'. The advantage of deriving from the
'ICustomPlugIn' interface is to have a common entrypoint for all plugin's, so
our plugin-manager can load the plugin's w/o knowing it's 'real' interface.
implementation
uses
Dialogs;
{ TSamplePlugIn }
constructor TSamplePlugIn.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
end;
destructor TSamplePlugIn.Destroy;
begin
inherited;
end;
function TSamplePlugIn.GetAuthor: string;
begin
Result := 'Marc Hoffmann';
end;
function TSamplePlugIn.GetName: string;
begin
Result := CS_NAME;
end;
function SamplePlugIn.GetDescription: string;
begin
Result := 'Place_your_description_here';
end;
function SamplePlugIn.GetVersion: string;
begin
Result := CS_VERSION;
end;
And last but not least, our core methods:
function SamplePlugIn.Sum(a, b: Integer): Integer;
begin
Result := a + b;
end;
procedure SamplePlugIn.DisplayMessage(const MyText: string);
begin
ShowMessage(MyText);
end;
end.
That's all! The only missing thing is our (core) library unit.
Sample PlugIn Library
=======================
library SamplePlugIn;
uses
ShareMemRep,
Windows,
Classes,
PlugInIntf in 'PlugInIntf.pas',
SamplePlugInIntf in 'SamplePlugInIntf.pas',
SamplePlugInImpl in 'SamplePlugInImpl.pas';
{$E plg} //
{$R *.res}
procedure DllMain(Reason: Integer) ;
begin
case Reason of
{ our 'dll' will be unloaded immediantly, so free up the shared
datamodule, if created before! }
DLL_PROCESS_DETACH:
{ place your code here! }
end;
end;
{-----------------------------------------------------------------------------
Procedure: RegisterPlugIn
Arguments: AOwner: TComponent; ForceCreate: Boolean = False
Result: TSamplePlugIn
-----------------------------------------------------------------------------
Description:
'RegisterPlugIn' is the one-and-only exported dll function to register a
plugin. This method will be called from the plugin-manager used by the
host to load and register the underlying plugin interface.
-----------------------------------------------------------------------------}
function RegisterPlugIn(AOwner: TComponent;
ForceCreate: Boolean = False): TSamplePlugIn; stdcall;
begin
Result := TSamplePlugIn.Create(AOwner);
end;
exports RegisterPlugIn;
{ ---------------------------------------------------------------------------
To be able to initialize some other stuff before registering the plugin
(e.g. creating a datamodule), we're hooking the load/unload-process of the
dll by deligating the necessary calls to our own method.
-----------------------------------------------------------------------------}
begin
{ attach our own dll loader }
DllProc := @DllMain;
DllProc(DLL_PROCESS_ATTACH);
end.
As you've may noticed, there's a unit called 'ShareMemRep' I've used above.
This is a very nice replacement of Borland's 'ShareMem' except that you
don't need to include the additional 'dll' in your app.
Look at: http://www.delphipages.com/result.cfm?ID=4166
Now, it's time to write our plugin-manager (the trickiest part 8-)
The PlugIn-Manager
====================
The plugin-manager is the core unit used by the host (your application) to
load & unload the plugin's. You can load as most plugin's as you want, but
beware of one - most important - point:
EVERY SUCESSFULLY LOADED PLUGIN MUST BE UNLOADED IN IT'S CONVERSE ORDER
BEFORE YOUR MAIN APPLICATION DESTROYS, O/W YOU'LL GET AN ACCESS VIOLATION.
This AV comes from the tricky part of the plugin loading-mechanism and should
be handled with care. If you bear in mind with the unloading procedure,
everything works fine (Tip: use try/except/finally blocks).
Okay, here it is, the first part:
unit PlugInMng;
interface
uses
Classes, Windows, PlugInIntf;
type
{ TPlugInStruct }
TPlugInStruct = packed record
AClass: TPlugInClass;
GUID: TGUID;
Handle: THandle;
AInterface: Pointer;
FileName: string;
end;
PPlugInStruct = ^TPlugInStruct;
{ TPlugInManager }
TPlugInManager = class(TComponent)
private
FPlugIns: TList;
FLastError: string;
FOwner: TComponent;
function GetPlugIn(Index: Integer): PPlugInStruct;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function GetLastError: string;
function LoadPlugIn(const AFileName: string; PlugInGUID: TGUID;
out PlugInIntf; ForceCreate: Boolean = False): Boolean;
function UnloadPlugIn(var PlugInIntf): Boolean;
function FindPlugIn(var PlugInIntf): PPlugInStruct;
function Count: Integer;
property PlugIns[Index: Integer]: PPlugInStruct read GetPlugIn;
end;
It's nothing special. I've included some helper methods to load, unload, count
and access plugins:
implementation
uses
SysUtils;
{ TPlugInManager }
constructor TPlugInManager.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FOwner := AOwner;
FLastError := '';
FPlugIns := TList.Create;
end;
destructor TPlugInManager.Destroy;
begin
FPlugIns.Free;
inherited;
end;
function TPlugInManager.GetLastError: string;
begin
Result := FLastError;
end;
function TPlugInManager.LoadPlugIn(const AFileName: string;
PlugInGUID: TGUID; out PlugInIntf; ForceCreate: Boolean = False): Boolean;
var
FileName: string;
DLLHandle: THandle;
FuncPtr: TFarProc;
PlugInProc: TPlugInProc;
PlugInClass: TPlugInClass;
PlugInStruct: PPlugInStruct;
begin
{ initialize variables }
Result := False;
FileName := AFileName;
DLLHandle := 0;
try
{ try to load passed dll }
DLLHandle := LoadLibrary(PAnsiChar(FileName));
if DLLHandle 0 then
begin
{ get function address of 'RegisterPlugIn' }
FuncPtr := GetProcAddress(DLLHandle, 'RegisterPlugIn');
if FuncPtr nil then
begin
{ assign register method }
@PlugInProc := FuncPtr;
{ create plugin instance }
PlugInClass := TPlugInClass(PlugInProc(FOwner, ForceCreate)); // creates instance!
{ the only tricky-part: accessing the common interface }
if assigned(PlugInClass) and (PlugInClass.GetInterface(PlugInGUID, PlugInIntf)) then
begin
{ save plugin properties }
New(PlugInStruct);
PlugInStruct.AClass := PlugInClass;
PlugInStruct.GUID := PlugInGUID;
PlugInStruct.Handle := DLLHandle;
PlugInStruct.AInterface := Pointer(PlugInIntf);
PlugInStruct.FileName := AFileName;
FPlugIns.Add(PlugInStruct);
Result := True;
end
else
FreeLibrary(DLLHandle);
end;
end; // try/finally
except
on e: Exception do
begin
FLastError := e.Message;
if DLLHandle 0 then
FreeLibrary(DLLHandle);
end;
end; // try/except
end;
function TPlugInManager.UnloadPlugIn(var PlugInIntf): Boolean;
var
i: Integer;
PlugInStruct: PPlugInStruct;
begin
Result := False;
try
for i := FPlugIns.Count - 1 downto 0 do
begin
PlugInStruct := FPlugIns[i];
if PlugInStruct^.AInterface = Pointer(PlugInIntf) then
begin
{ tricky: beware of the right order!!! }
Pointer(PlugInIntf) := nil;
PlugInStruct.AClass.Free;
FreeLibrary(PlugInStruct^.Handle);
Dispose(PlugInStruct);
FPlugIns.Delete(i);
Result := True;
end;
end;
except
raise;
end;
end;
function TPlugInManager.Count: Integer;
begin
Result := FPlugIns.Count;
end;
function TPlugInManager.GetPlugIn(Index: Integer): PPlugInStruct;
begin
Result := nil;
if Index FPlugIns.Count - 1 then
Exit;
Result := PPlugInStruct(FPlugIns[Index]);
end;
end.
Loading/Unloading the PlugIn (Sample Client)
==============================================
We've done it! The only missing thing now is, how to load & access our
plugin?? Take a look at this code snippet:
program SampleClient;
uses
ShareMemRep,
SysUtils,
Windows,
PlugInMng,
SamplePlugInIntf in 'SamplePlugInIntf.pas';
{$R *.res}
resourcestring
RS_PLUGIN_ERROR =
'Error while loading plugin "%s"!';
var
PlugInManager: TPlugInManager;
SamplePlugIn: ISamplePlugIn;
FileName: string;
Loaded: Boolean;
begin
{ initialize variables }
Loaded := False;
{ create plugin manager }
PlugInManager := TPlugInManager.Create(nil);
try
{ set filename to our plugin }
FileName := ExtractFilePath(ParamStr(0)) + 'SamplePlugIn.plg';
try
{ try to load the plugin }
Loaded := PlugInManager.LoadPlugIn(FileName, ISamplePlugIn, SamplePlugIn);
if not Loaded then
raise Exception.CreateFmt(RS_PLUGIN_ERROR, [ExtractFileName(FileName)]);
{ CALL OUR CORE METHODS }
SamplePlugIn.DisplayMessage(SamplePlugIn.Sum(10, 5));
except
on e: Exception do
MessageBox(0, PChar(e.message), PAnsiChar(CS_NAME),
MB_ICONERROR);
end;
if Loaded then
PlugInManager.UnloadPlugIn(SamplePlugIn);
finally // wrap up
PlugInManager.Free;
end; // try/finally
end.
CONCLUSION
============
As you've may noticed, my solution of creating plugin's has tried to take
all advantages as descripted earlier:
+ You have a well-designed interface to call your methods
+ You only need the 'Intf'-unit (NOT the 'real' implementation)
+ You don't need to compile your app with runtime packages (bpl's)
It's also possible to use the plugin-manager inside your plugin's to load
other plugin's, ...
I hope, you've enjoyed to read this document and feel free to extend my
concept 8-)))
DOWNLOADS
===========
- Simple PlugIn Demo
- Multi PlugIn Demo