Gehe zu deutscher Webseite

ViaThinkSoft CodeLib

This article is in:
CodeLibProgramming aidsDelphi

Für Anwendungen, bei denen Module benötigt werden, bietet sich eine DLL an. Allerdings ist die Kommunikation zwischen Host-Anwendung und DLL meist schwierig und die Objektorientierten Techniken sowie die Typsicherheit gehen aufgrund der atomaren Datentypen verloren. In dieser Vorlage zeige ich, wie man eine in der DLL erzeugte Klasse in der Hostanwendung nutzen kann.

Zuerst einmal benötigen wir ein Interface, das sowohl von der DLL als auch von der Hostanwendung genutzt wird.

ModuleInterface.pas

unit ModuleInterface;

interface

type
  IMyModule = interface(IInterface)
    procedure SetName(Name: string);
    procedure SayHello;
  end;

implementation

end.

Nun erstellen wir eine Beispiel-Implementierung A. Später wird jede Implementierung ein Modul unserer Hostanwendung sein.

ModuleImplementationA.pas

unit ModuleImplementationA;

interface

uses
  ModuleInterface;

type
  TModuleA = class(TInterfacedObject, IMyModule)
  private
    FName: string;
  public
    procedure SetName(Name: string);
    procedure SayHello;
  end;

implementation

uses
  Dialogs;

{ TModuleA }

procedure TModuleA.SayHello;
begin
  ShowMessageFmt('TModuleA: Hello, my name is %s!', [FName]);
end;

procedure TModuleA.SetName(Name: string);
begin
  FName := Name;
end;

end.

Diese Implementierung wird nun von unserer ModuleA.DLL verwendet. Alle unsere Modul-DLLs werden eine Instanz der implementierenden Klasse erzeugen und den Zeiger darauf zurückgeben.

ModuleA.pas

library ModuleA;

uses
  ModuleInterface in 'ModuleInterface.pas',
  ModuleImplementationA in 'ModuleImplementationA.pas';

{$R *.res}

function CreateInstance(): IMyModule;
begin
  result := TModuleA.Create();
end;

exports
  CreateInstance;

end.

Unser erstes Modul ModuleA.DLL ist nun fertig! Wir widmen uns jetzt der Hostanwendung.

Um den Gebrauch der Modul-DLL einfacher zu gestalten, habe ich folgende Unit entwickelt, die das Verwalten, also das dynamische Einbinden sowie das Freigeben aller geladenen DLLs erleichtert und den Code in der Hostanwendung auf das wesentliche Beschränkt.

ModuleManager.pas

unit ModuleManager;

interface

uses
  SysUtils, Windows, Contnrs, ModuleInterface;

type
  TModule = class(TObject)
  public
    Handle: THandle;
    constructor Create();
    destructor Destroy(); override;
  end;

  TModuleManager = class(TObject)
  private
    LoadedModules: TObjectList; // contains TModule
  public
    function LoadModule(AFilename: string): IMyModule;
    constructor Create();
    destructor Destroy(); override;
  end;

implementation

type
  TDLLFunction = function(): IMyModule;

  EDLLException = class(Exception);

{ TModuleManager }

constructor TModuleManager.Create;
begin
  inherited;

  LoadedModules := TObjectList.Create;
  LoadedModules.OwnsObjects := true;
end;

destructor TModuleManager.Destroy;
begin
  LoadedModules.Destroy;

  inherited;
end;

function TModuleManager.LoadModule(AFilename: string): IMyModule;
var
  theFunction: TDLLFunction;
  buf: array [0..144] of Char;
  Module: TModule;
const
  functionname = 'CreateInstance';
resourcestring
  LNG_LINK_ERROR = 'Unable to link to function %s in DLL %s!';
  LNG_DLL_LOAD_ERROR = 'Unable to load DLL %s!' {+ #13#10 + 'Reason: %s.'};
begin
  Module := TModule.Create;
  Module.Handle := LoadLibrary(StrPCopy(buf, AFilename));
  LoadedModules.Add(Module);

  if Module.Handle <> 0 then
  begin
    @theFunction := GetProcAddress(Module.Handle, StrPCopy(buf, functionname));

    if @theFunction <> nil then
    begin
      Result := theFunction()
    end
    else
    begin
      raise EDLLException.CreateFmt(LNG_LINK_ERROR, [functionname, AFilename]);
    end;
  end
  else
  begin
    raise EDLLException.CreateFmt(LNG_DLL_LOAD_ERROR,
      [AFilename{, DLLErrors[FHDLL]}]);
  end;
end;

{ TModule }

constructor TModule.Create;
begin
  inherited;
end;

destructor TModule.Destroy;
begin
  if Handle <> 0 then
  begin
    FreeLibrary(Handle);
  end;

  inherited;
end;

end.

Nun können wir unsere Hostanwendung designen. Wir nehmen hierfür ein leeres VCL Projekt, erzeugen einen Button und fügen im OnClick-Ereignis unseren Testcode ein:

uses
  ModuleManager, ModuleInterface;

procedure TMainForm.Button1Click(Sender: TObject);
var
  ModuleManager: TModuleManager;
  Module: IMyModule;
begin
  ModuleManager := TModuleManager.Create;
  try
    Module := ModuleManager.LoadModule('ModuleA.dll');
    try
      Module.SetName('Johnny');
      Module.SayHello;
    finally
      // Wichtig! Der Referenzzähler wird verringert.
      // Das Objekt wird jetzt und nicht am Funktionsende
      // (bei dem die DLL entladen ist) freigegeben!
      Module := nil;
    end;
  finally
    ModuleManager.Free;
  end;
end;

Wie jetzt ein einzelnes Modul oder Plugin gestaltet wird, ist jedem selbst überlassen. Es können auch eigene VCL-Forms in die DLL eingebunden werden (demnächst mehr darüber). Ich denke, es bietet sich an, dass Module in DLL-Form in ein dafür vorgesehenes Verzeichnis kopiert werden und dort bei jedem Programmstart gesucht und automatisch eingebunden werden. Ebenso denke ich, dass es sinnvoll ist, wenn jede DLL nur 1 Klasse anbietet. Sollte ein Modul aus mehreren Teilmodulen (wie z.B. Downloadmanager, Uploadmanager, Datenbankmanager, etc.) bestehen, können diese einzelnen Klasseninstanzen als Felder in der Modul-Basis-Klasse integriert werden.

Viel Spaß!

Für Borland C++ Builder siehe http://edn.embarcadero.com/article/20165
Daniel Marschall
ViaThinkSoft Co-Founder