Search code examples
delphipluginsmodular

Delphi - Creating Well-Designed Plug-In's


I found working well code snippet that allows easily create modular Delphi application. This code is working well with Delphi 5 version.

Link to snippet -> http://delphi.cjcsoft.net/viewthread.php?tid=44129

I have big Delphi 5's project build over the years and I started to wonder if it would be possible to load Plugin (*.plg) compiled with newer version of Delphi (in my case XE2).

I replaced ShareMemRep.pas with Borland's ShareMem because XE2 can't compile with ShareMemRep. Delphi 5 version of plugin and client app that is loading plugins written in Delphi 5 working well so far.

I used the same code (except that I changed PAnsiChar to PWideChar where I had to) and compiled plugin and client app with Delphi XE2. But compiled client app can't load neither plugin compiled with Delphi XE2 nor plugin compiled with Delphi 5.

Also Delphi 5 client app can't load plugin compiled with Delphi XE2.

Delphi XE2 client is not loading compiled with Delphi XE2 plugin because (PlugInClass.GetInterface(PlugInGUID, PlugInIntf)) is returning False.

When Delphi XE2 or Delphi 5 client app is loading opposite version of plugin compilation then (PlugInClass.GetInterface(PlugInGUID, PlugInIntf)) causes Access Violation.

What I found from System.pas that some changes where made in bottom of the line but my knowledge is pure about that kind of 'hack bits'.

Have any one some knowledge whether it is possible to load plugin (library) compiled with different version than app that is loading that plugin using code from snippet?

EDIT: My source code: https://bitbucket.org/plum/delphimodularapp

Source code off plugin loading (manager):

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 }
    // Delphi XE
    //DLLHandle := LoadLibraryW(PWideChar(AFileName));
    // Loading *.plg plugin
    DLLHandle := LoadLibrary(PAnsiChar(AFileName));
    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) then begin
          // On that line I'm getting AV when I'm trying to load
          // plugin compiled with Delphi XE2 by Host application compiled with Delphi5.
          if (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;
        end;

        if Result = False then begin
          FreeLibrary(DLLHandle);
        end;
      end;
    end;    // try/finally
  except
    on e: Exception do
    begin
      FLastError := e.Message;
      if DLLHandle <> 0 then
        FreeLibrary(DLLHandle);
    end;
  end;    // try/except
end;

Code of Plugin (Library):

library libSamplePlugIn;

uses
  ShareMem,
  Windows,
  Classes,
  uPlugInIntf in 'uPlugInIntf.pas',
  uSamplePlugInIntf in 'uSamplePlugInIntf.pas',
  uSamplePlugInImpl in 'uSamplePlugInImpl.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;

function RegisterPlugIn(AOwner: TComponent;
  ForceCreate: Boolean = False): TSamplePlugIn; stdcall;
begin
  Result := TSamplePlugIn.Create(AOwner);
end;

exports RegisterPlugIn;

begin
end.

And Plugin class:

unit uSamplePlugInImpl;

interface

uses
  uPlugInIntf, uSamplePlugInIntf, 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 GetVersion: string;
    function GetDescription: string;
    function Sum(a, b: Integer): Integer;
  end;

implementation

{ TSamplePlugIn }

constructor TSamplePlugIn.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
end;

{ Rest of implementation ...}

function TSamplePlugIn.Sum(a, b: Integer): Integer;
begin
  Result := a + b;
end;

end.

Solution

  • This code is broken due to a fatal design flaw. You cannot pass a Delphi class across a DLL boundary and have it mean something on the other side. That rule is broken when you pass a TComponent to the DLL, and when it returns a TSamplePlugIn.

    Your main options:

    1. Switch to runtime packages, or
    2. Use interfaces instead of classes.

    You don't appear to be far off. Just stop passing that TComponent. Find another way to manage lifetime. Interfaces already provide reference counting for that. And return an interface rather than a class instance. Then you should be on your way.

    You have indeed just followed the lead set by that article. Sadly it appears to have been written by somebody without sufficient expertise. It might have appeared to work in D5, but it was broken there too. You just got away with it somehow.

    Note that PChar is PWideChar in Unicode Delphi and PAnsiChar in ANSI Delphi. Using that fact would allow you to write code that works in both.

    DLLHandle := LoadLibrary(PChar(AFileName));
    

    The DllMain is spurious. Remove it.