Mega Code Archive

 
Categories / Delphi / Examples
 

Creating Well Designed Plug Ins

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