This is my attempt at explaining how to create Delphi custom single package, why would you want to do this you ask?, well using packages comes with some advantages.
Compiler Speed.
Smaller EXE's & DLL's.
DLL plug ins etc, very easy.
But unfortunately if you compile just using the standard packages,
you end up having to re-distribute lots of BPL's.
Now what
would be nice, is the advantages of BPL's without the dis-advantages
of BPL hell. And this document will hopefully help explain how to do
this.
The BPL we will be creating is RunTime only, you could
create a RunTime/DesignTime package but this does require it uses at
least the VCL packages so for now we'll just create a Runtime
only.
Also note this document is based on Delphi 7, but should
work with earlier versions too.
You might not know this, but if you compile with packages and create a simple form, your application will in fact use 2 BPL's, and this is just for a Form. You can see this by pressing F8 to run your program then goto Project|Information for project. Here you will see that RTL70 & VCL70 are required. Now load up one of you big projects that uses other third party component, and compile with packages, and see how many packages are are required now. As you can see keeping track of all these, and having to redistribute them etc becomes a pain.
Now when creating a custom package, what we don't want is any VCL
or Third party units getting compiled into our DLL & EXE's, these
units will want to be placed inside our custom package. So first
thing to do is remove any references to any VCL or Third party units.
Goto Tools|Environment Options and make a copy of the Library path
for later, once you have a copy of the Library replace it with just
the BPL directory. Your Environment Dialog should look similar
to->
The
reason we blank out the Library path and only leave the BPL directory
is because when compiling with Packages our EXE don't require the
them, and doing so will make sure our package gets compiled with all
the required units that our DLL & EXE's require. But we still
need the BPL directory so Delphi can find our new BPL.
Ok, lets create our package. As you found out earlier even
creating a Delphi App that has just a Form on it will require 2
BPL's, where going to create our own package that will make it only
require 1. Goto File|New|Other and select package. Now save this
package and call it MyPkg, you will notice that Delphi would have
automatically added the RTL.DCP to the requires section remove this,
as were trying to create a single package. If you try to compile this
package you should get an error about System.pas not found, don't
worry this is what we want. The Library path we made a copy of
earlier paste this into the the packages
Options|Directories|Conditionals Search path. Now if you try and
compile the package again it should compile without problems. So far
this package is pretty useless it has no components at all included,
so the next stage is adding our required components. Now the easy way
to do this is by trying to compile an Application using our newly
created Pkg, from a standard blank Delphi program with a Form, goto
Project|Options|Packages and tick the Build with packages checkbox
and place into the textbox MyPkg. Something like->
If
you try and compile this project you should get an error that it
cannot find sysinit.dcu, now this is the only unit that cannot be
placed into a package, most likely because it's the unit that's
required to maintain packages in the first place. Now a simple
solution to this is to place the SysInit.dcu file into your
Projects/BPL directory as this is still on your Environment|Library
Path. SysInit.Dcu can be found in your Delphi/Lib Directory make a
copy of this an place in your Delphi/Project/Bpl directory. Now
recompile again, if everything works as expected it should now
complain that Forms.dcu cannot be found. Now this give us a hint to
what unit's are required inside out Custom Package :), So from inside
our Custom Package add a unit called MyPkgUnits and simply add the
Forms to the uses Clause. eg->
unit myPkgUnits; interface uses forms; implementation end.
Re-Compile the package, Delphi will them come up with a Dialog saying
that VCL and RTL packages are required to make it compatible with
other installed packages, well we don't want it to be compatible with
other installed packages so press the cancel button here, another
dialog will then appear saying that if these changes are not applied
errors will occur, press the Yes buttons as errors will not occur,
unfortunately every time you re-compile this package Delphi is going
to come up with these 2 dialogs, I don't know of any way to get
around this unfortunately.
Now go back to your Project and
re-compile, if everything works as expected it now should compile
without any problems. And if you press F8 and look at the required
BPL's there should be just the 1 single package called MyPkg.
Of
course your not going to just create programs that use Forms and a
couple of standard components, so now open up a more complicated
application or add some third party components to the test project
and re-compile. Delphi will automatically complain that it cannot
find some DCU or PAS file make a note of these units and simple add
them to the uses clause of the MyPkgUnit.pas, re-compile the package
then the application, if it complains about another unit just keep on
adding them to the package, and re-do until the EXE compiles without
errors.
Example-> lets add a TnxTable to our Test form, if
we re-compile it should complain that it cannot find db.dcu. Now we
could add db to our uses clause in our MyPkgUnit.pas, but actually we
can save some time here by adding nxDb to our uses clause because
Delphi will then implicitly add db for us. So now re-compile our
package remembering to press Cancel then Yes to the dialogs that
appear. Recompile our Project, and heh presto, we have a Delphi form
that's got a TnxTable on it and still only using a single package. If
you look at the EXE & BPL you will notice how small the EXE is
and how big the Package is, on mine the EXE is 27K & the BPL is
about 1.5Meg, the BPL is large because implicitly it has compiled
into a lot of VCL code. If like me you will end up with a one Big Bpl
that you use for all your application, currently mine is about 14Meg,
but this includes a lot of Third party components. A little trick if
you want to build a package with all your components you will ever
use, just simple create a dummy project and add all the components to
a form, and do the compile-recompile package procedure.
One
thing to keep an eye on, when adding third party components etc,
Delphi will automatically alter your Environment/Library path, keep
an eye on this and make sure it's only got the BPL directory listed.
As mentioned earlier using a single package, makes making a DLL plug in system easy. First you might ask why would you want to use DLL's, as you might have found it's possible to use BPL's as plug ins. Well one problem with BPL's is that they all have to share the same Namespace, what I mean by this is all unit names have to be unique, IOW: you couldn't have a Unit called main.pas in 2 BPL's and have them loaded at the same time. Using DLL's you get your own private namespace, this is very handy as you can just copy an existing plug in you've created and slightly modify it and you'll be able to load them both without worrying about Unit name conflicts. In fact in this article I'm going to do just that, I'll show a very simple plug in system that loads some DLL's that contain a Form, each DLL will be just a copy that we'll slightly change so that the form is different, adding a few controls and maybe changing the color etc. Later you will also see how mixing DLL's and interfaces are a match made in heaven. Also you will notice we don't even have to use the Exports directive.
First thing we need is some sort of plug in manager. So create a file called myPluginManager.pas and paste the following code. Then add this to your single custom package and Build.
unit myPluginManager;
interface
uses windows,classes,sysutils,dialogs;
type
TPluginManager = class
private
fPlugins:TInterfaceList;
public
procedure LoadDLLs(mask:string);
property Plugins:TInterfaceList read fPlugins;
procedure AddPlugin(I:IInterface);
constructor Create;
destructor Destroy; override;
end;
function PluginManager:TPluginManager;
implementation
var
_PluginManager:TPluginManager;
function PluginManager:TPluginManager;
begin
if not assigned(_PluginManager) then
_PluginManager:=TPluginManager.Create;
result:=_PluginManager;
end;
{ TPluginManager }
procedure TPluginManager.AddPlugin(I: IInterface);
begin
fPlugins.Add(I);
end;
constructor TPluginManager.Create;
begin
inherited Create;
fPlugins:=TInterfaceList.Create;
end;
destructor TPluginManager.Destroy;
begin
fPlugins.Free;
inherited;
end;
procedure TPluginManager.LoadDLLs(mask: string);
var
sr:TSearchRec;
ff:string;
begin
if FindFirst(mask,0,sr) = 0 then begin
repeat
ff:=includetrailingbackslash(extractfilepath(mask))+sr.name;
if LoadLibrary(pchar(ff)) = 0 then
RaiseLastWin32Error;
until FindNext(sr)<>0;
end;
end;
initialization
finalization
if assigned(_PluginManager) then
_PluginManager.free;
end.
The reason we add it to our custom package is because we want this to be in the same namespace as the EXE & DLL's. IOW: Our Plugin manager is common to all DLL's & EXE.
Earler I mentioned about interfaces, now these are very handy when creating plugins, especially as your plugin system grows. We will use interfaces to expose what our DLL supports. So copy the following code and save as MyPluginFormInterface.pas
unit myPluginFormInterface;
interface
uses forms;
type
IMyPluginFormInterface = interface
['{03D6BB1C-A31C-4D47-AAA3-06C8CD94ADB4}']
function CreateForm:TForm;
function PluginName:string;
end;
implementation
end.
Here you will see I've created 2 simple functions that our DLL will need to implement so that it supports our Plugin Form, the PluginName we will use to give it a nice name that we can show, and CreateForm will be to create an instance of our Form. Note: this unit does'nt need to be compiled into our Custom package but it does need to be accessable from our EXE & DLL's.
Ok, here we will create our EXE that later will load up our DLL plug ins. So goto Delphi and create a new Application called TestPlugin. Add a TMainMenu with a menu item called Plugins. In the uses clause add myPluginManager & myPluginFormInterface. And on the OnCreate event add the following code->
procedure TForm1.FormCreate(Sender: TObject);
var
lp:integer;
PF:IMyPluginFormInterface;
m:TMenuItem;
begin
pluginmanager.LoadDLLs(includetrailingbackslash(extractfilepath(paramstr(0)))+'*.dll');
for lp:=0 to PluginManager.Plugins.Count-1 do begin
if supports(PluginManager.Plugins[lp],IMyPluginFormInterface,PF) then
begin
m:=TMenuItem.Create(self);
m.tag:=lp;
m.Caption:=PF.PluginName;
m.OnClick:=PluginMenuClick;
plugins1.Add(m);
end;
end;
end;
Also add a method to the form called PluginMenuClick(Sender: TObject);
procedure TForm1.PluginMenuClick(Sender: TObject);
var
f:TForm;
PF:IMyPluginFormInterface;
m:TMenuItem;
begin
m:=Sender as TMenuItem;
if supports(PluginManager.Plugins[m.tag],IMyPluginFormInterface,PF) then begin
f:=PF.CreateForm;
if not f.visible then f.show;
end;
end;
Now compile this EXE making sure you compile it with your Custom Package. If you run this not much will happen yet, as we've yet to create our Plugins. :)
Ok, now here's the fun part, lets create some plugins. Create a new project using Delphi's DLL Wizard, save this in a directory under our EXE called plugin1, also you may as well save the project as plugin1. Delphi's DLL Wizard give you some comments about DLL memory management & Sharemem, ignore it!! in fact why not delete the comment as our DLL plugin wont have this problem :)
The part that stops us having to use sharemen is you also make sure you compile this DLL using our Custom Package, so goto project/Options/packages and make sure were going to be compile this DLL with our Custom Package too. The reason this works is because our DLL & EXE are using our Custom Package there going to be using the same Instance of the Memory Manager and as such ShareMen is not needed, neat eh?. Also while in Project/Options change the Output Directory to ..\ to make our DLL's get placed in our EXE's directory.
Because our plug ins are going to show some Forms, lets make a form, so goto File/New/Form modify this form change it's color & caption etc. Set the forms name to fMyForm and save as MyForm inside our Plugin1 directory.
Now lets make our DLL a plug in.. :) Paste the following code into the Projects Source->
library plugin1;
uses
SysUtils,
Classes,
forms,
myPluginManager,
myPluginFormInterface,
myForm in 'myForm.pas' {fMyForm};
{$R *.res}
type
TMyPlugin = class(TInterfacedObject,ImyPluginFormInterface)
function CreateForm:TForm;
function PluginName:string;
end;
{ TMyPlugin }
function TMyPlugin.CreateForm: TForm;
begin
result:=TfMyForm.Create(nil);
end;
function TMyPlugin.PluginName: string;
begin
result:='My Plugin Form One';
end;
begin
PluginManager.AddPlugin(TMyPlugin.Create);
end.
Now compile your DLL, if everything works as expected you should have a DLL called Plugin1.dll in your EXE's directory arround 18K in size.
Go back to your EXE project and run, if everything went well you should now see a menu option under plugins called "My Plugin Form One" if you click this it should then create an instance of your Form.
Now the bit I really like :), now make a copy of your Plugin1 directory, and say call it Plugin2, Open the Project and Save the Project as Plugin2, modify your fmyForm form, eg. change it's color etc. Here you might want to change the return of PluginName so that it appears differently on the menu. Compile and run you Main EXE, you now should have 2 menu options that create 2 different looking forms. You may also want to use explorer and delete the plugin1.Dpr etc from the directory.
Of course the example is not that much use, but hopefully you can see how easy it is to expand on for use in a real live system.
Ok, that's it, I hope you find it usefull, and before you know it, you'll be doing much more fancy plug ins that this :). In writing this article I've tried to keep things as simple as possible, there are lots of places this system could be enhanced, eg. the ability to keep a cache of supported interfaces that a perticalar DLL supports, here you could then Load DLL's on demand etc. Using reference counted Objects it could also be possible to dynamically unload DLL's too , but I'll maybe leave that too another day.
Regards, Keith Johnson [NDX];